This commit is contained in:
2019-09-05 09:02:20 +02:00
commit adc01486ec
19 changed files with 403 additions and 0 deletions

14
Pipfile Normal file
View File

@@ -0,0 +1,14 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
pytest = "*"
python-dateutil = "*"
click = "*"
[requires]
python_version = "3.7"

123
Pipfile.lock generated Normal file
View File

@@ -0,0 +1,123 @@
{
"_meta": {
"hash": {
"sha256": "ecd9551140ee3d10e0a0efe9489ca58e21466e0ecbe6da511f95e3f6483c3e44"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"atomicwrites": {
"hashes": [
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
],
"version": "==1.3.0"
},
"attrs": {
"hashes": [
"sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
"sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
],
"version": "==19.1.0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"index": "pypi",
"version": "==7.0"
},
"importlib-metadata": {
"hashes": [
"sha256:9ff1b1c5a354142de080b8a4e9803e5d0d59283c93aed808617c787d16768375",
"sha256:b7143592e374e50584564794fcb8aaf00a23025f9db866627f89a21491847a8d"
],
"markers": "python_version < '3.8'",
"version": "==0.20"
},
"more-itertools": {
"hashes": [
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
],
"version": "==7.2.0"
},
"packaging": {
"hashes": [
"sha256:a7ac867b97fdc07ee80a8058fe4435ccd274ecc3b0ed61d852d7d53055528cf9",
"sha256:c491ca87294da7cc01902edbe30a5bc6c4c28172b5138ab4e4aa1b9d7bfaeafe"
],
"version": "==19.1"
},
"pluggy": {
"hashes": [
"sha256:0825a152ac059776623854c1543d65a4ad408eb3d33ee114dff91e57ec6ae6fc",
"sha256:b9817417e95936bf75d85d3f8767f7df6cdde751fc40aed3bb3074cbcb77757c"
],
"version": "==0.12.0"
},
"py": {
"hashes": [
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
],
"version": "==1.8.0"
},
"pyparsing": {
"hashes": [
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
],
"version": "==2.4.2"
},
"pytest": {
"hashes": [
"sha256:95d13143cc14174ca1a01ec68e84d76ba5d9d493ac02716fd9706c949a505210",
"sha256:b78fe2881323bd44fd9bd76e5317173d4316577e7b1cddebae9136a4495ec865"
],
"index": "pypi",
"version": "==5.1.2"
},
"python-dateutil": {
"hashes": [
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
],
"index": "pypi",
"version": "==2.8.0"
},
"six": {
"hashes": [
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
],
"version": "==1.12.0"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
}
},
"develop": {}
}

1
README.rst Normal file
View File

@@ -0,0 +1 @@
...

0
app/__init__.py Normal file
View File

48
app/add.py Normal file
View File

@@ -0,0 +1,48 @@
from datetime import datetime, date, time
from dateutil import parser
from typing import List, Union, Tuple
from types_ import Minutes
def subtract_times(first: time, second: time) -> Minutes:
return int(
(
datetime.combine(datetime(1, 1, 1, 0, 0, 0), second)
- datetime.combine(date.today(), first)
).seconds
/ 60
)
def add_up_timesheet(timesheet_string: str) -> Minutes:
total_minutes = 0
for line in timesheet_string.split("\n"):
time_range_strings = find_time_ranges(line)
if time_range_strings:
for time_range_string in time_range_strings:
start_string, end_string = split_time_range_string(time_range_string)
start, end = map(parse_time, [start_string, end_string])
total_minutes += subtract_times(start, end)
return total_minutes
def find_time_ranges(timesheet_line: str) -> Union[list, List[str]]:
remove_hashes_and_leading_trailing_spaces = timesheet_line.replace("#", "").strip()
try:
parse_time(remove_hashes_and_leading_trailing_spaces)
except ValueError:
return []
else:
if ":" not in remove_hashes_and_leading_trailing_spaces:
return []
return remove_hashes_and_leading_trailing_spaces.split(", ")
def parse_time(time_string: str) -> time:
return parser.parse(time_string).time()
def split_time_range_string(time_range_string) -> Tuple[str]:
return time_range_string.split("-")

11
app/cli.py Normal file
View File

@@ -0,0 +1,11 @@
import click
from app.add import add_up_timesheet
@click.command()
@click.argument("filename", type=click.Path(exists=True))
def cli(filename):
with open(filename) as fin:
timesheet_string = fin.read()
click.echo(add_up_timesheet(timesheet_string))

1
config.py Normal file
View File

@@ -0,0 +1 @@
SAMPLE_TIMESHEET_PATH = "tests/timesheet.md"

View File

@@ -0,0 +1,19 @@
Metadata-Version: 1.1
Name: markdown-timesheet
Version: 0.1.7
Summary: A quick way to add up time entries in a markdown document.
Home-page: https://github.com/zevaverbach/markdown_timesheet
Author: Zev Averbach
Author-email: zev@averba.ch
License: MIT license
Description: ...
Keywords: markdown timesheet
Platform: UNKNOWN
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7

View File

@@ -0,0 +1,12 @@
README.rst
setup.cfg
setup.py
app/__init__.py
app/add.py
app/cli.py
markdown_timesheet.egg-info/PKG-INFO
markdown_timesheet.egg-info/SOURCES.txt
markdown_timesheet.egg-info/dependency_links.txt
markdown_timesheet.egg-info/entry_points.txt
markdown_timesheet.egg-info/not-zip-safe
markdown_timesheet.egg-info/top_level.txt

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,4 @@
[console_scripts]
add=app.cli:cli

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
app

2
setup.cfg Normal file
View File

@@ -0,0 +1,2 @@
[flake8]
max-line-length = 99

41
setup.py Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""The setup script."""
from setuptools import setup, find_packages
with open("README.rst") as readme_file:
readme = readme_file.read()
test_requirements = ["pytest"]
setup(
author="Zev Averbach",
author_email="zev@averba.ch",
classifiers=[
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
description="A quick way to add up time entries in a markdown document.",
license="MIT license",
long_description=readme,
include_package_data=True,
keywords="markdown timesheet",
name="markdown_timesheet",
packages=find_packages(include=["app"]),
test_suite="tests",
tests_require=test_requirements,
entry_points="""
[console_scripts]
add=app.cli:cli
""",
url="https://github.com/zevaverbach/markdown_timesheet",
version="0.1.7",
zip_safe=False,
)

0
tests/__init__.py Normal file
View File

71
tests/test_add.py Normal file
View File

@@ -0,0 +1,71 @@
from datetime import time
from pytest import fixture
from app.add import (
split_time_range_string,
add_up_timesheet,
find_time_ranges,
parse_time,
subtract_times,
)
from config import SAMPLE_TIMESHEET_PATH
@fixture
def timesheet() -> str:
with open(SAMPLE_TIMESHEET_PATH) as fin:
return fin.read()
@fixture
def string_with_no_times():
return "## no times here 123"
@fixture
def time_range_strings():
return ["9:30-12:30", "8:30-7:30"]
@fixture
def time_ranges():
return [["9:30", "12:30"], ["8:30", "7:30"]]
@fixture
def times():
return [(time(9, 30), time(12, 30)), (time(8, 30), time(7, 30))]
@fixture
def string_with_times():
return "## 9:30-12:30, 8:30-7:30 "
def test_add_up_timesheet(timesheet):
assert add_up_timesheet(timesheet) == 430
def test_find_time_ranges_none(string_with_no_times):
assert find_time_ranges(string_with_no_times) == []
def test_find_time_ranges(string_with_times, time_range_strings):
assert find_time_ranges(string_with_times) == time_range_strings
def test_split_time_range_string(time_range_strings, time_ranges):
for time_range_string, time_range_tuple in zip(time_range_strings, time_ranges):
assert split_time_range_string(time_range_string) == time_range_tuple
def test_parse_time(time_ranges, times):
for time_range_tuple, time_tuple in zip(time_ranges, times):
for time_string, time_ in zip(time_range_tuple, time_tuple):
assert parse_time(time_string) == time_
def test_subtract_times(times):
assert subtract_times(*times[0]) == 180
assert subtract_times(*times[1]) == 1380

50
tests/timesheet.md Normal file
View File

@@ -0,0 +1,50 @@
# 30 Aug - 1 Sep 2019
## 6.5 hours total
invoiced on 2 Sep 2019 (#6766)
Gathering requirements, defining scope of work, producing specification and implementation timeline
documents.
# 2 Sep 2019
TOTAL: 430 minutes
## 9:45-10:15
Researching and answering Evgeny's question about Mailwizz, SES, and email metrics reporting.
## 10:50-12:54
Step 3: Making a Jinja template for the newsletter.
## 12:54-13:17, 14:04-14:12
Step 4: Test sending the newsletter.
## 14:12-14:19
Project scaffolding, installing dependencies, setting up remote git repository.
## 14:19-15:05
More step 4:
- writing tests for the render function
- setting up data for test using sample newsletter
- make sure special characters like "&" are escaped (they are in the instantiation of env in
render.py)
## 15:05-15:33, 15:42-16:27
Step 6 and 7: Make dummy allocation list and write a function to send a rendered newsletter.
- making one small media CSV
- making one small allocation CSV
- making buckets on my own account (not Syllabus) for testing
## 16:38-16:56
- writing to Evgeny to update about rendering from templates
## 16:56-17:05
Step 7
- uploading small media CSV to bucket
- writing function
## 17:05-17:44
Adding a few features to admin page spec, asking clarifying questions.
Writing "status" email.
## 19:55-20:48
Writing CSV-getting and -parsing for users, media, and allocation

3
types_.py Normal file
View File

@@ -0,0 +1,3 @@
from typing import NewType
Minutes = NewType("Minutes", int)