Skip to content

Commit

Permalink
Global refactoring
Browse files Browse the repository at this point in the history
 * Extracting ui (views and components)
 * Extracting core functions
 * redesigning dependencies (especially gi)
  • Loading branch information
StephaneMangin committed Oct 6, 2020
1 parent 278ca58 commit dc279aa
Show file tree
Hide file tree
Showing 34 changed files with 2,441 additions and 2,383 deletions.
4 changes: 2 additions & 2 deletions benchmark.py
Expand Up @@ -9,8 +9,8 @@
pkgdir = os.path.join(os.path.dirname(__file__), 'src')
sys.path.insert(0, pkgdir)

from gtimelog.settings import Settings
from gtimelog.timelog import parse_datetime, TimeLog
from gtimelog.core.settings import Settings
from gtimelog.core.timelog import parse_datetime, TimeLog


fns = []
Expand Down
5 changes: 3 additions & 2 deletions gtimelog
@@ -1,13 +1,14 @@
#!/usr/bin/python3
#!/usr/bin/env python
"""
Script to run GTimeLog from the source checkout without installing
"""
import os
import sys

from gtimelog.main import main

basedir = os.path.dirname(os.path.realpath(__file__))
pkgdir = os.path.join(basedir, 'src')
sys.path.insert(0, pkgdir)

from gtimelog.main import main # noqa: E402
main()
7 changes: 6 additions & 1 deletion src/gtimelog/__init__.py
@@ -1,3 +1,8 @@
# The gtimelog package.
import logging
import sys

__version__ = '0.12.0.dev0'
DEBUG = '--debug' in sys.argv
root_logger = logging.getLogger()
root_logger.addHandler(logging.StreamHandler())

Empty file added src/gtimelog/core/__init__.py
Empty file.
2 changes: 2 additions & 0 deletions src/gtimelog/core/exceptions.py
@@ -0,0 +1,2 @@
class EmailError(Exception):
pass
10 changes: 2 additions & 8 deletions src/gtimelog/secrets.py → src/gtimelog/core/services.py
Expand Up @@ -2,18 +2,12 @@
Keyring and secrets
"""
import functools
import logging
from gettext import gettext as _

from .utils import require_version

require_version('Gtk', '3.0')
require_version('Soup', '2.4')
require_version('Secret', '1')
from gi.repository import Gio, Gtk, GObject, Soup, Secret

from gtimelog import root_logger

log = logging.getLogger('gtimelog.secrets')
log = root_logger.getChild('services')


def start_smtp_password_lookup(server, username, callback):
Expand Down
4 changes: 1 addition & 3 deletions src/gtimelog/settings.py → src/gtimelog/core/settings.py
Expand Up @@ -10,9 +10,7 @@

from configparser import RawConfigParser


from gtimelog.timelog import parse_time

from gtimelog.core.utils import parse_time

legacy_default_home = os.path.normpath('~/.gtimelog')
default_config_home = os.path.normpath('~/.config')
Expand Down
125 changes: 2 additions & 123 deletions src/gtimelog/timelog.py → src/gtimelog/core/timelog.py
Expand Up @@ -8,135 +8,14 @@
import collections
import csv
import datetime
import os
import socket
import re
from collections import defaultdict
from hashlib import md5
from operator import itemgetter


def as_minutes(duration):
"""Convert a datetime.timedelta to an integer number of minutes."""
return duration.days * 24 * 60 + duration.seconds // 60


def as_hours(duration):
"""Convert a datetime.timedelta to a float number of hours."""
return duration.days * 24.0 + duration.seconds / (60.0 * 60.0)


def format_duration(duration):
"""Format a datetime.timedelta with minute precision."""
h, m = divmod(as_minutes(duration), 60)
return '%d h %d min' % (h, m)


def format_duration_short(duration):
"""Format a datetime.timedelta with minute precision."""
h, m = divmod((duration.days * 24 * 60 + duration.seconds // 60), 60)
return '%d:%02d' % (h, m)


def format_duration_long(duration):
"""Format a datetime.timedelta with minute precision, long format."""
h, m = divmod((duration.days * 24 * 60 + duration.seconds // 60), 60)
if h and m:
return '%d hour%s %d min' % (h, h != 1 and "s" or "", m)
elif h:
return '%d hour%s' % (h, h != 1 and "s" or "")
else:
return '%d min' % m


def parse_datetime(dt):
"""Parse a datetime instance from 'YYYY-MM-DD HH:MM' formatted string."""
if len(dt) != 16 or dt[4] != '-' or dt[7] != '-' or dt[10] != ' ' or dt[13] != ':':
raise ValueError('bad date time: %r' % dt)
try:
year = int(dt[:4])
month = int(dt[5:7])
day = int(dt[8:10])
hour = int(dt[11:13])
min = int(dt[14:])
except ValueError:
raise ValueError('bad date time: %r' % dt)
return datetime.datetime(year, month, day, hour, min)


def parse_time(t):
"""Parse a time instance from 'HH:MM' formatted string."""
m = re.match(r'^(\d+):(\d+)$', t)
if not m:
raise ValueError('bad time: %r' % t)
hour, min = map(int, m.groups())
return datetime.time(hour, min)


def virtual_day(dt, virtual_midnight):
"""Return the "virtual day" of a timestamp.
Timestamps between midnight and "virtual midnight" (e.g. 2 am) are
assigned to the previous "virtual day".
"""
if dt.time() < virtual_midnight: # assign to previous day
return dt.date() - datetime.timedelta(1)
return dt.date()


def different_days(dt1, dt2, virtual_midnight):
"""Check whether dt1 and dt2 are on different "virtual days".
See virtual_day().
"""
return virtual_day(dt1, virtual_midnight) != virtual_day(dt2,
virtual_midnight)


def first_of_month(date):
"""Return the first day of the month for a given date."""
return date.replace(day=1)


def prev_month(date):
"""Return the first day of the previous month."""
if date.month == 1:
return datetime.date(date.year - 1, 12, 1)
else:
return datetime.date(date.year, date.month - 1, 1)


def next_month(date):
"""Return the first day of the next month."""
if date.month == 12:
return datetime.date(date.year + 1, 1, 1)
else:
return datetime.date(date.year, date.month + 1, 1)


def uniq(items):
"""Return list with consecutive duplicates removed."""
result = items[:1]
for item in items[1:]:
if item != result[-1]:
result.append(item)
return result


def get_mtime(filename):
"""Return the modification time of a file, if it exists.
Returns None if the file doesn't exist.
"""
# Accept any file-like object instead of a filename (for the benefit of
# unit tests).
if hasattr(filename, 'read'):
return None
try:
return os.stat(filename).st_mtime
except OSError:
return None

from gtimelog.core.utils import different_days, as_minutes, as_hours, format_duration_short, format_duration_long, \
get_mtime, virtual_day, parse_datetime, first_of_month, next_month

Entry = collections.namedtuple('Entry', 'start stop duration tags entry')

Expand Down
187 changes: 187 additions & 0 deletions src/gtimelog/core/utils.py
@@ -0,0 +1,187 @@
import datetime
from email.header import Header
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import os
import re
import time
from gettext import gettext as _

from gtimelog import __version__, DEBUG


if DEBUG:
def mark_time(what=None, _prev=[0, 0]):
t = time.time()
if what:
print("{:.3f} ({:+.3f}) {}".format(t - _prev[1], t - _prev[0], what))
else:
print()
_prev[1] = t
_prev[0] = t
else:
def mark_time(what=None):
pass


def as_minutes(duration):
"""Convert a datetime.timedelta to an integer number of minutes."""
return duration.days * 24 * 60 + duration.seconds // 60


def as_hours(duration):
"""Convert a datetime.timedelta to a float number of hours."""
return duration.days * 24.0 + duration.seconds / (60.0 * 60.0)


def format_duration(duration):
"""Format a datetime.timedelta with minute precision."""
h, m = divmod(as_minutes(duration), 60)
return '%d h %d min' % (h, m)


def format_duration_short(duration):
"""Format a datetime.timedelta with minute precision."""
h, m = divmod((duration.days * 24 * 60 + duration.seconds // 60), 60)
return '%d:%02d' % (h, m)


def format_duration_long(duration):
"""Format a datetime.timedelta with minute precision, long format."""
h, m = divmod((duration.days * 24 * 60 + duration.seconds // 60), 60)
if h and m:
return '%d hour%s %d min' % (h, h != 1 and "s" or "", m)
elif h:
return '%d hour%s' % (h, h != 1 and "s" or "")
else:
return '%d min' % m


def parse_datetime(dt):
"""Parse a datetime instance from 'YYYY-MM-DD HH:MM' formatted string."""
if len(dt) != 16 or dt[4] != '-' or dt[7] != '-' or dt[10] != ' ' or dt[13] != ':':
raise ValueError('bad date time: %r' % dt)
try:
year = int(dt[:4])
month = int(dt[5:7])
day = int(dt[8:10])
hour = int(dt[11:13])
min = int(dt[14:])
except ValueError:
raise ValueError('bad date time: %r' % dt)
return datetime.datetime(year, month, day, hour, min)


def parse_time(t):
"""Parse a time instance from 'HH:MM' formatted string."""
m = re.match(r'^(\d+):(\d+)$', t)
if not m:
raise ValueError('bad time: %r' % t)
hour, min = map(int, m.groups())
return datetime.time(hour, min)


def virtual_day(dt, virtual_midnight):
"""Return the "virtual day" of a timestamp.
Timestamps between midnight and "virtual midnight" (e.g. 2 am) are
assigned to the previous "virtual day".
"""
if dt.time() < virtual_midnight: # assign to previous day
return dt.date() - datetime.timedelta(1)
return dt.date()


def different_days(dt1, dt2, virtual_midnight):
"""Check whether dt1 and dt2 are on different "virtual days".
See virtual_day().
"""
return virtual_day(dt1, virtual_midnight) != virtual_day(dt2,
virtual_midnight)


def first_of_month(date):
"""Return the first day of the month for a given date."""
return date.replace(day=1)


def prev_month(date):
"""Return the first day of the previous month."""
if date.month == 1:
return datetime.date(date.year - 1, 12, 1)
else:
return datetime.date(date.year, date.month - 1, 1)


def next_month(date):
"""Return the first day of the next month."""
if date.month == 12:
return datetime.date(date.year + 1, 1, 1)
else:
return datetime.date(date.year, date.month + 1, 1)


def uniq(items):
"""Return list with consecutive duplicates removed."""
result = items[:1]
for item in items[1:]:
if item != result[-1]:
result.append(item)
return result


def get_mtime(filename):
"""Return the modification time of a file, if it exists.
Returns None if the file doesn't exist.
"""
# Accept any file-like object instead of a filename (for the benefit of
# unit tests).
if hasattr(filename, 'read'):
return None
try:
return os.stat(filename).st_mtime
except OSError:
return None


def format_duration(duration):
"""Format a datetime.timedelta with minute precision.
The difference from gtimelog.timelog.format_duration() is that this
one is internationalized.
"""
h, m = divmod(as_minutes(duration), 60)
return _('{0} h {1} min').format(h, m)


def isascii(s):
return all(0 <= ord(c) <= 127 for c in s)


def address_header(name_and_address):
if isascii(name_and_address):
return name_and_address
name, addr = parseaddr(name_and_address)
name = str(Header(name, 'UTF-8'))
return formataddr((name, addr))


def subject_header(header):
if isascii(header):
return header
return Header(header, 'UTF-8')


def prepare_message(sender, recipient, subject, body):
if isascii(body):
msg = MIMEText(body)
else:
msg = MIMEText(body, _charset="UTF-8")
if sender:
msg["From"] = address_header(sender)
msg["To"] = address_header(recipient)
msg["Subject"] = subject_header(subject)
msg["User-Agent"] = "gtimelog/{}".format(__version__)
return msg

0 comments on commit dc279aa

Please sign in to comment.