PK ! gcal2redmine/__init__.pyPK ! Ĩ gcal2redmine/cli.py#!/usr/bin/env python3 from re import sub, subn import click from .gcal import GoogleCalendar from .redmine import RedmineTimeTracking from .utils import format_date, strike, validate_date @click.command() @click.option("-d", "--dry-run", is_flag=True, help="Print only, make no action.") @click.option("-y", "--yes", is_flag=True, help="Do not prompt for confirmation.") @click.option( "--no-mark", is_flag=True, help="Do not mark events as processed in Calendar." ) @click.option("--no-track", is_flag=True, help="Do not add track entry in Redmine.") @click.option( "-f", "--from-date", callback=validate_date, default="today", help="Must be in format: YYYY-MM-DD, today, yesterday [default=today]", ) @click.option( "-t", "--to-date", callback=validate_date, default="today", help="Must be in format: YYYY-MM-DD, today, yesterday [default=today]", ) def main(dry_run, yes, no_mark, no_track, from_date, to_date): gcal = GoogleCalendar(from_date, to_date) redmine = RedmineTimeTracking() events = [] total_time = 0.0 # get and show all events to be added for event_id, spent_on, event, hours in gcal.events: issue_id = subn(r"^.*#(\d+).*$", r"\1", event) comments = sub(r"#\d+$", r"", event) spent_on = format_date(spent_on).date() if issue_id[1]: click.echo( "{} | #{}: {} (time: {})".format(spent_on, issue_id[0], comments, hours) ) events.append([event_id, issue_id[0], hours, spent_on, comments]) total_time += hours click.echo("Total time: {}".format(total_time)) # ask for confirmation before adding if (yes or click.confirm("Do you want to proceed?")) and not dry_run: with click.progressbar(events) as events_bar: for event in events_bar: event_id = event.pop(0) if not no_track: redmine.add_time(*event) if not no_mark: gcal.mark_event(event_id) click.echo("Events successfully added.") else: click.echo("Nothing have been done.") if __name__ == "__main__": main() PK ! H H gcal2redmine/gcal.py#!/usr/bin/env python3 from argparse import ArgumentParser from datetime import datetime, time, timedelta from os import environ from pathlib import Path from re import sub from googleapiclient import discovery import httplib2 from oauth2client import client, tools from oauth2client.file import Storage from tzlocal import get_localzone from .utils import format_date class GoogleCalendar: def __init__(self, from_date="today", to_date="today"): """Instanciate attributes.""" self.config_dir = ( Path(environ.get("G2C_CONFIG_HOME", default="~/.config/g2c")) .expanduser() .resolve() ) self.secret_file = Path(self.config_dir, "google.secret.json") self.token_file = Path(self.config_dir, "google.token.json") self.scopes = "https://www.googleapis.com/auth/calendar" self.application_name = "gcal2redmine" self.service = self._get_service() self.events = self._get_events(from_date, to_date) def _get_service(self): """Gets valid user credentials from storage. If nothing has been stored, or if the stored credentials are invalid, the OAuth2 flow is completed to obtain the new credentials. """ Path(self.config_dir).mkdir(mode=0o700, parents=True, exist_ok=True) store = Storage(self.token_file) credentials = store.get() if not credentials or credentials.invalid: flow = client.flow_from_clientsecrets(self.secret_file, self.scopes) flow.user_agent = self.application_name class FakeFlags: logging_level = "INFO" noauth_local_webserver = False auth_host_port = [8080, 8090] auth_host_name = "localhost" flags = FakeFlags() credentials = tools.run_flow(flow, store, flags) print("Storing credentials to {}".format(self.token_file)) http = credentials.authorize(httplib2.Http()) service = discovery.build("calendar", "v3", http=http) return service def _get_events(self, from_date="today", to_date="today"): local_tz = get_localzone() today_morning = datetime.combine(datetime.now().date(), time(0, 0, 0)) today = local_tz.localize(today_morning).isoformat() # start date is assumed to be beginning of the specified day if from_date == "today": start = today elif from_date == "yesterday": start = local_tz.localize(today_morning - timedelta(days=1)).isoformat() else: from_date_morning = datetime(*from_date, 0, 0, 0) start = local_tz.localize(from_date_morning).isoformat() # end date is assumed to be end of the specified day if to_date == "today": end = local_tz.localize(today_morning + timedelta(days=1)).isoformat() elif to_date == "yesterday": end = today else: to_date_evening = datetime(*to_date, 0, 0, 0) + timedelta(days=1) end = local_tz.localize(to_date_evening).isoformat() events = list() events_params = { "calendarId": "primary", "timeMin": start, "timeMax": end, "singleEvents": True, "orderBy": "startTime", } events_result = self.service.events().list(**events_params).execute() events_items = events_result.get("items", []) for event_item in events_items: start = event_item["start"].get("dateTime", event_item["start"].get("date")) end = event_item["end"].get("dateTime", event_item["end"].get("date")) duration = format_date(end) - format_date(start) duration_float = duration.seconds / 3600.0 events.append( (event_item["id"], start, event_item["summary"], duration_float) ) return events def mark_event(self, event_id): event = ( self.service.events().get(calendarId="primary", eventId=event_id).execute() ) summary = sub(r"^(.*#)(\d+)$", r"\1!\2", event["summary"]) event["summary"] = summary marked_event = ( self.service.events() .update(calendarId="primary", eventId=event["id"], body=event) .execute() ) return marked_event["id"] PK ! r! gcal2redmine/redmine.py#!/usr/bin/env python3 from json import load as json_load from pathlib import Path from os import environ from redminelib import Redmine class RedmineTimeTracking: def __init__(self): """Instanciate attributes.""" self.config_dir_path = environ.get("G2C_CONFIG_HOME", default="~/.config/g2c") self.config_file_path = ( Path(self.config_dir_path, "redmine.json").expanduser().resolve() ) self.config = self._get_config(self.config_file_path) self.redmine = self._get_redmine(self.config) def _get_config(self, path): """Get config parameters. :param path: Path to the JSON configuration file :type path: str :return: Configuration parameters :rtype: dict """ config = dict() with open(path) as config_fh: config = json_load(config_fh) return config def _get_redmine(self, config): """Get Redmine resource object. :param config: Connection parameters :type config: dict :return: Redmine connection :rtype: redminelib.Redmine """ return Redmine(**config) def _get_issue(self, redmine, issue_id): """Get Redmine issue resource object. :param redmine: Redmine connection object :type redmine: Redmine :param issue_id: Redmine issue ID :type issue_id: int :return: Redmine issue object :rtype: redminelib.resources.Issue """ return self.redmine.issue.get(issue_id) def _get_project(self, project_id, include=None): """Get Redmine project resource object :param project_id: Redmine project ID :type project_id: int :param include: Optional fields to include in project entry, defaults to None :param include: str, optional :return: Redmine project object :rtype: redminelib.resources.Project """ return self.redmine.project.get(project_id, include=include) def add_time(self, issue_id, hours, spent_on, comments): """Add time tracking entry into the given Redmine issue. :param issue_id: Redmine issue ID :type issue_id: int :param hours: Duration of the tasks :type hours: float :param spent_on: Date of the tasks :type spent_on: datetime.date :param comments: Description of the task :type comments: str """ issue = self._get_issue(self.redmine, issue_id) project = self._get_project(issue.project.id, include="time_entry_activities") activity_id = project.time_entry_activities[0]["id"] # TODO: proper handling of activities # for activity in project.time_entry_activities: # if activity["name"].lower() == activity.lower(): # activity_id = activity["id"] return self.redmine.time_entry.create( issue_id=issue_id, hours=hours, spent_on=spent_on, comments=comments, activity_id=activity_id, ) PK ! L gcal2redmine/utils.py#!/usr/bin/env python3 from datetime import datetime from re import sub import click def format_date(date_time): """Convert gcal date into datetime object.""" return datetime.strptime(sub(r"\+\d+:\d+$", "", date_time), "%Y-%m-%dT%H:%M:%S") def validate_date(ctx, param, value): try: if value.lower() in ("today", "yesterday"): return value.lower() else: y, m, d = map(int, value.split("-")) return (y, m, d) except ValueError: raise click.BadParameter("must be YYYY-MM-DD, today, yesterday") def strike(text): result = str() for c in text: result += c + "\u0336" return result PK !H/F. 6 - gcal2redmine-0.2.0.dist-info/entry_points.txtN+I/N.,()JON1*JMKE%dZ&fqq PK !HڽT U " gcal2redmine-0.2.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK !Hz % gcal2redmine-0.2.0.dist-info/METADATA[o@+歭d;`H,HVP0I2cb/:)_/yj;g|v':gh,*(zk&1"c"2\ZQ8JJfN1q!`B,8.a FK`MB ,@g%%+zNފg,]<6+ogm6_Yf;hÚgZ06Y͖}qj_D$WtLZs$[|BKAꘫ,1!9=`ѪAYheҥˉ;]NNR sғu(k&I+?= ä+4!|xnŤgM<\miv}%I:w`ot? ؽ(ou1dgGxs= s8?ѮTJoI4'4:#ҌV*zП^;nk4{PK !H # gcal2redmine-0.2.0.dist-info/RECORD}ͻ@ ~epa8 (LPGWOD5{VILI|cLr`
T:i\0n߯VZ ^" 9lc36NZ[KľQvh֕Վi
x0fFZ,n7/=PZecV ^t%RϰoTMݨO98̹32*Mݵ3qrqWخ?PK ! gcal2redmine/__init__.pyPK ! Ĩ 6 gcal2redmine/cli.pyPK ! H H gcal2redmine/gcal.pyPK ! r! r gcal2redmine/redmine.pyPK ! L &