first
This commit is contained in:
14
Pipfile
Normal file
14
Pipfile
Normal 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
123
Pipfile.lock
generated
Normal 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
1
README.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
...
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
48
app/add.py
Normal file
48
app/add.py
Normal 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
11
app/cli.py
Normal 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))
|
||||||
19
markdown_timesheet.egg-info/PKG-INFO
Normal file
19
markdown_timesheet.egg-info/PKG-INFO
Normal 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
|
||||||
12
markdown_timesheet.egg-info/SOURCES.txt
Normal file
12
markdown_timesheet.egg-info/SOURCES.txt
Normal 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
|
||||||
1
markdown_timesheet.egg-info/dependency_links.txt
Normal file
1
markdown_timesheet.egg-info/dependency_links.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
4
markdown_timesheet.egg-info/entry_points.txt
Normal file
4
markdown_timesheet.egg-info/entry_points.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
[console_scripts]
|
||||||
|
add=app.cli:cli
|
||||||
|
|
||||||
1
markdown_timesheet.egg-info/not-zip-safe
Normal file
1
markdown_timesheet.egg-info/not-zip-safe
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
markdown_timesheet.egg-info/top_level.txt
Normal file
1
markdown_timesheet.egg-info/top_level.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
app
|
||||||
41
setup.py
Normal file
41
setup.py
Normal 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
0
tests/__init__.py
Normal file
71
tests/test_add.py
Normal file
71
tests/test_add.py
Normal 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
50
tests/timesheet.md
Normal 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
|
||||||
Reference in New Issue
Block a user