Compare commits
25 Commits
11888b4208
...
b5c15f5d9a
Author | SHA1 | Date | |
---|---|---|---|
b5c15f5d9a | |||
d775b4ad0e | |||
d8f6c6bf65 | |||
907faf4f99 | |||
9b0142eae8 | |||
1624b1d04d | |||
c9bfad20ce | |||
c746afa926 | |||
e7dec35e2f | |||
b3dd253f47 | |||
54fdb537be | |||
5eb44f0e71 | |||
06dd687a98 | |||
66ba20d629 | |||
f118ff5d99 | |||
a5f3945d78 | |||
261501de09 | |||
a1aa629566 | |||
4eade4216f | |||
614aa18027 | |||
a8bb4aacf1 | |||
ec560cbba6 | |||
b486d05658 | |||
5eac80c5aa | |||
20cb6f38c9 |
15
README.md
15
README.md
|
@ -31,7 +31,20 @@ And the ([Hamster](https://github.com/projecthamster/hamster)) manual time track
|
||||||
![](docs/obligatory-hamster.png)
|
![](docs/obligatory-hamster.png)
|
||||||
|
|
||||||
|
|
||||||
# License
|
## Dependencies
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -39,7 +39,7 @@ from .data import (
|
||||||
WorkSample,
|
WorkSample,
|
||||||
)
|
)
|
||||||
from .format import cli, icalendar
|
from .format import cli, icalendar
|
||||||
from .source import git_repo, csv_file
|
from .source import csv_file, git_repo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -51,7 +51,8 @@ MINUTE = datetime.timedelta(minutes=1)
|
||||||
|
|
||||||
|
|
||||||
def filter_samples(
|
def filter_samples(
|
||||||
samples: list[WorkSample], sample_filter: set[str],
|
samples: list[WorkSample],
|
||||||
|
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)]
|
||||||
|
@ -66,10 +67,16 @@ def heuristically_realize_samples(
|
||||||
* No samples overlap.
|
* No samples overlap.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
previous_sample_end = datetime.datetime.fromtimestamp(0, datetime.UTC)
|
previous_sample_end = None
|
||||||
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
|
||||||
|
@ -120,9 +127,17 @@ 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
|
||||||
|
@ -141,9 +156,9 @@ def load_samples(args):
|
||||||
)
|
)
|
||||||
del csv_path
|
del csv_path
|
||||||
|
|
||||||
|
|
||||||
return shared_time_stamps_set
|
return shared_time_stamps_set
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
|
|
||||||
|
@ -173,5 +188,6 @@ 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, file='./output/samples.ics',
|
shared_time_stamps,
|
||||||
|
file=args.output_file,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
__version__ = '0.1.18'
|
__version__ = '0.1.19'
|
||||||
|
|
|
@ -47,7 +47,8 @@ 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', 'mailto:' + label_and_type.removeprefix('author:'),
|
'organizer',
|
||||||
|
'mailto:' + label_and_type.removeprefix('author:'),
|
||||||
)
|
)
|
||||||
|
|
||||||
cal.add_component(event)
|
cal.add_component(event)
|
||||||
|
|
|
@ -1,60 +1,102 @@
|
||||||
import argparse
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from decimal import Decimal
|
|
||||||
import datetime
|
import datetime
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from typing import Any
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from decimal import Decimal
|
||||||
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
|
||||||
|
|
||||||
def iterate_samples_from_dicts(rows: list[dict]) -> Iterator[WorkSample]:
|
@dataclasses.dataclass
|
||||||
max_title_parts = 2
|
class PossibleKeys:
|
||||||
|
time_start: list[str]
|
||||||
for event_data in rows:
|
time_end: list[str]
|
||||||
|
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
|
||||||
possible_time_keys = [
|
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)
|
||||||
]
|
]
|
||||||
possible_duration_keys = [
|
duration_keys = [
|
||||||
k for k, v in event_data.items() if isinstance(v, Decimal) and 'duration_seconds' in k
|
k
|
||||||
|
for k, v in event_data.items()
|
||||||
|
if isinstance(v, Decimal) and 'duration_seconds' in k
|
||||||
]
|
]
|
||||||
possible_name_keys = [k for k, v in event_data.items() if isinstance(v, str)]
|
name_keys = [k for k, v in event_data.items() if isinstance(v, str)]
|
||||||
possible_image_keys = [
|
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)
|
||||||
]
|
]
|
||||||
|
|
||||||
possible_misc_keys = list(event_data.keys())
|
misc_keys = list(event_data.keys())
|
||||||
for k in possible_image_keys:
|
for k in image_keys:
|
||||||
if k in possible_misc_keys:
|
if k in misc_keys:
|
||||||
possible_misc_keys.remove(k)
|
misc_keys.remove(k)
|
||||||
del k
|
del k
|
||||||
for k in possible_time_keys :
|
for k in time_keys:
|
||||||
if k in possible_misc_keys:
|
if k in misc_keys:
|
||||||
possible_misc_keys.remove(k)
|
misc_keys.remove(k)
|
||||||
del k
|
del k
|
||||||
|
|
||||||
date = event_data[possible_time_keys[0]] if possible_time_keys else None
|
time_start_keys = [k for k in time_keys if 'start' in k.lower() ]
|
||||||
image = event_data[possible_image_keys[0]] if possible_image_keys else None
|
time_end_keys = [k for k in time_keys if 'end' in k.lower() or 'stop' in k.lower() ]
|
||||||
|
|
||||||
if date is None:
|
return PossibleKeys(
|
||||||
continue
|
time_start = time_start_keys,
|
||||||
|
time_end = time_end_keys,
|
||||||
|
duration = duration_keys,
|
||||||
|
name = name_keys,
|
||||||
|
image = image_keys,
|
||||||
|
misc = misc_keys,
|
||||||
|
)
|
||||||
|
|
||||||
if len(possible_duration_keys) > 0:
|
def start_end(sample: dict[str,Any], keys: PossibleKeys) -> tuple[datetime.datetime | None, datetime.datetime | None]:
|
||||||
start_at = date
|
if keys.time_start and keys.time_end:
|
||||||
seconds = event_data[possible_duration_keys[0]]
|
return (sample[keys.time_start[0]], sample[keys.time_end[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(event_data[k] for k in possible_name_keys[max_title_parts:])
|
description = '\n\n'.join(
|
||||||
|
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(
|
||||||
|
@ -65,6 +107,9 @@ def iterate_samples_from_dicts(rows: list[dict]) -> 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)
|
||||||
yield from iterate_samples_from_dicts(dicts)
|
samples = list(iterate_samples_from_dicts(dicts))
|
||||||
|
assert len(samples) > 0, 'Did not found any samples'
|
||||||
|
yield from samples
|
||||||
|
|
|
@ -39,10 +39,12 @@ 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, tz=datetime.UTC,
|
commit.authored_date,
|
||||||
|
tz=datetime.UTC,
|
||||||
)
|
)
|
||||||
committed_date = datetime.datetime.fromtimestamp(
|
committed_date = datetime.datetime.fromtimestamp(
|
||||||
commit.committed_date, tz=datetime.UTC,
|
commit.committed_date,
|
||||||
|
tz=datetime.UTC,
|
||||||
)
|
)
|
||||||
|
|
||||||
yield WorkSample(
|
yield WorkSample(
|
||||||
|
|
Reference in New Issue
Block a user