1
0

Compare commits

..

No commits in common. "b5c15f5d9a0adb079092983d3cedd990fc9df513" and "11888b4208f06c60bb24ab2256e2a20a0c58b8c6" have entirely different histories.

6 changed files with 57 additions and 134 deletions

View File

@ -31,20 +31,7 @@ And the ([Hamster](https://github.com/projecthamster/hamster)) manual time track
![](docs/obligatory-hamster.png) ![](docs/obligatory-hamster.png)
## Dependencies # License
All requirements can be installed easily using:
```bash
pip install -r requirements.txt
```
Full list of requirements:
- [GitPython](https://pypi.org/project/GitPython/)
- [icalendar](https://pypi.org/project/icalendar/)
## License
``` ```
MIT License MIT License

View File

@ -39,7 +39,7 @@ from .data import (
WorkSample, WorkSample,
) )
from .format import cli, icalendar from .format import cli, icalendar
from .source import csv_file, git_repo from .source import git_repo, csv_file
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -51,8 +51,7 @@ MINUTE = datetime.timedelta(minutes=1)
def filter_samples( def filter_samples(
samples: list[WorkSample], samples: list[WorkSample], sample_filter: set[str],
sample_filter: set[str],
) -> list[WorkSample]: ) -> list[WorkSample]:
assert len(sample_filter) > 0 assert len(sample_filter) > 0
return [s for s in samples if set(s.labels).intersection(sample_filter)] return [s for s in samples if set(s.labels).intersection(sample_filter)]
@ -67,16 +66,10 @@ def heuristically_realize_samples(
* No samples overlap. * No samples overlap.
""" """
previous_sample_end = None previous_sample_end = datetime.datetime.fromtimestamp(0, datetime.UTC)
for sample in samples: for sample in samples:
end_at = sample.end_at end_at = sample.end_at
if previous_sample_end is None:
if end_at.tzinfo:
previous_sample_end = datetime.datetime.fromtimestamp(0, datetime.UTC)
else:
previous_sample_end = datetime.datetime.fromtimestamp(0)
assert previous_sample_end <= end_at, 'Iterating in incorrect order' assert previous_sample_end <= end_at, 'Iterating in incorrect order'
# TODO: Allow end_at is None # TODO: Allow end_at is None
@ -127,17 +120,9 @@ def parse_arguments():
default='cli_report', default='cli_report',
choices=['cli_report', 'icalendar'], choices=['cli_report', 'icalendar'],
) )
parser.add_argument(
'--out',
action='store',
type=Path,
dest='output_file',
default='output/samples.ics',
)
return parser.parse_args() return parser.parse_args()
def load_samples(args):
def load_samples(args) -> set[WorkSample]:
shared_time_stamps_set: set[WorkSample] = set() shared_time_stamps_set: set[WorkSample] = set()
# Git repositories # Git repositories
@ -156,8 +141,8 @@ def load_samples(args) -> set[WorkSample]:
) )
del csv_path del csv_path
return shared_time_stamps_set
return shared_time_stamps_set
def main(): def main():
logging.basicConfig() logging.basicConfig()
@ -188,6 +173,5 @@ def main():
sys.stdout.write(t) sys.stdout.write(t)
elif args.format_mode == 'icalendar': elif args.format_mode == 'icalendar':
icalendar.generate_icalendar_file( icalendar.generate_icalendar_file(
shared_time_stamps, shared_time_stamps, file='./output/samples.ics',
file=args.output_file,
) )

View File

@ -1 +1 @@
__version__ = '0.1.19' __version__ = '0.1.18'

View File

@ -47,8 +47,7 @@ def generate_calendar(
for label_and_type in sample.labels: for label_and_type in sample.labels:
if label_and_type.startswith('author:'): if label_and_type.startswith('author:'):
event.add( event.add(
'organizer', 'organizer', 'mailto:' + label_and_type.removeprefix('author:'),
'mailto:' + label_and_type.removeprefix('author:'),
) )
cal.add_component(event) cal.add_component(event)

View File

@ -1,102 +1,60 @@
import datetime import argparse
import urllib.parse
from typing import Any
from collections.abc import Iterator from collections.abc import Iterator
from decimal import Decimal from decimal import Decimal
import datetime
import urllib.parse
from pathlib import Path from pathlib import Path
import dataclasses
from personal_data.util import load_csv_file from personal_data.util import load_csv_file
from ..data import WorkSample from ..data import WorkSample
@dataclasses.dataclass def iterate_samples_from_dicts(rows: list[dict]) -> Iterator[WorkSample]:
class PossibleKeys: max_title_parts = 2
time_start: list[str]
time_end: list[str] for event_data in rows:
duration: list[str]
name: list[str]
image: list[str]
misc: list[str]
def determine_possible_keys(event_data: dict[str, Any]) -> PossibleKeys:
# Select data # Select data
time_keys = [ possible_time_keys = [
k for k, v in event_data.items() if isinstance(v, datetime.date) k for k, v in event_data.items() if isinstance(v, datetime.date)
] ]
duration_keys = [ possible_duration_keys = [
k k for k, v in event_data.items() if isinstance(v, Decimal) and 'duration_seconds' in k
for k, v in event_data.items()
if isinstance(v, Decimal) and 'duration_seconds' in k
] ]
name_keys = [k for k, v in event_data.items() if isinstance(v, str)] possible_name_keys = [k for k, v in event_data.items() if isinstance(v, str)]
image_keys = [ possible_image_keys = [
k for k, v in event_data.items() if isinstance(v, urllib.parse.ParseResult) k for k, v in event_data.items() if isinstance(v, urllib.parse.ParseResult)
] ]
misc_keys = list(event_data.keys()) possible_misc_keys = list(event_data.keys())
for k in image_keys: for k in possible_image_keys:
if k in misc_keys: if k in possible_misc_keys:
misc_keys.remove(k) possible_misc_keys.remove(k)
del k del k
for k in time_keys: for k in possible_time_keys :
if k in misc_keys: if k in possible_misc_keys:
misc_keys.remove(k) possible_misc_keys.remove(k)
del k del k
time_start_keys = [k for k in time_keys if 'start' in k.lower() ] date = event_data[possible_time_keys[0]] if possible_time_keys else None
time_end_keys = [k for k in time_keys if 'end' in k.lower() or 'stop' in k.lower() ] image = event_data[possible_image_keys[0]] if possible_image_keys else None
return PossibleKeys( if date is None:
time_start = time_start_keys, continue
time_end = time_end_keys,
duration = duration_keys,
name = name_keys,
image = image_keys,
misc = misc_keys,
)
def start_end(sample: dict[str,Any], keys: PossibleKeys) -> tuple[datetime.datetime | None, datetime.datetime | None]: if len(possible_duration_keys) > 0:
if keys.time_start and keys.time_end: start_at = date
return (sample[keys.time_start[0]], sample[keys.time_end[0]]) seconds = event_data[possible_duration_keys[0]]
end_at = date + datetime.timedelta(seconds = float(seconds))
del seconds
else:
start_at = None
end_at = date
if keys.time_start and keys.duration:
start = sample[keys.time_start[0]]
duration = datetime.timedelta(seconds=float(sample[keys.duration[0]]))
return (start, start + duration)
if keys.time_start:
start = sample[keys.time_start[0]]
return (start, None)
if keys.time_end:
return (None, sample[keys.time_end[0]])
return (None, None)
def iterate_samples_from_dicts(rows: list[dict[str,Any]]) -> Iterator[WorkSample]:
assert len(rows) > 0
max_title_parts = 2
if True:
event_data = rows[len(rows)//2] # Hopefully select a useful representative.
possible_keys = determine_possible_keys(event_data)
del event_data
assert len(possible_keys.time_start) + len(possible_keys.time_end) >= 1
assert len(possible_keys.image) >= 0
for event_data in rows:
'''
title = ': '.join(event_data[k] for k in possible_name_keys[:max_title_parts]) title = ': '.join(event_data[k] for k in possible_name_keys[:max_title_parts])
description = '\n\n'.join( description = '\n\n'.join(event_data[k] for k in possible_name_keys[max_title_parts:])
event_data[k] for k in possible_name_keys[max_title_parts:]
)
image = event_data[possible_keys.image[0]] if possible_keys.image else None
'''
labels = [f'{k}:{event_data[k]}' for k in possible_misc_keys]
(start_at, end_at) = start_end(event_data, possible_keys)
labels = [f'{k}:{event_data[k]}' for k in possible_keys.misc]
# Create event # Create event
yield WorkSample( yield WorkSample(
@ -107,9 +65,6 @@ def iterate_samples_from_dicts(rows: list[dict[str,Any]]) -> Iterator[WorkSample
del event_data del event_data
def iterate_samples_from_csv_file(file_path: Path) -> Iterator[WorkSample]: def iterate_samples_from_csv_file(file_path: Path) -> Iterator[WorkSample]:
dicts = load_csv_file(file_path) dicts = load_csv_file(file_path)
samples = list(iterate_samples_from_dicts(dicts)) yield from iterate_samples_from_dicts(dicts)
assert len(samples) > 0, 'Did not found any samples'
yield from samples

View File

@ -39,12 +39,10 @@ def get_samples_from_project(repo: git.Repo) -> Iterator[WorkSample]:
labels.append('author:' + commit.author.email) labels.append('author:' + commit.author.email)
authored_date = datetime.datetime.fromtimestamp( authored_date = datetime.datetime.fromtimestamp(
commit.authored_date, commit.authored_date, tz=datetime.UTC,
tz=datetime.UTC,
) )
committed_date = datetime.datetime.fromtimestamp( committed_date = datetime.datetime.fromtimestamp(
commit.committed_date, commit.committed_date, tz=datetime.UTC,
tz=datetime.UTC,
) )
yield WorkSample( yield WorkSample(