Skip to content

Commit 3a27d05

Browse files
authored
Merge pull request #21 from cweidner3/develop
Develop
2 parents ef88103 + 211d1c9 commit 3a27d05

File tree

9 files changed

+367
-9
lines changed

9 files changed

+367
-9
lines changed

.github/workflows/lint-code.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Pylint
2+
3+
on: [push]
4+
5+
jobs:
6+
lint:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v2
11+
12+
- name: Set up Python 3.11
13+
uses: actions/setup-python@v1
14+
with:
15+
python-version: 3.11
16+
17+
- name: Install dependencies
18+
run: |
19+
python -m pip install --upgrade pip
20+
pip install pylint
21+
22+
- name: Analysing the code with pylint
23+
run: bash ./utils/lint.sh
24+
25+
- name: Store results output as artifact
26+
uses: actions/upload-artifact@v3
27+
with:
28+
name: pylint-output
29+
path: results.txt

.github/workflows/push-images.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ on:
44
push:
55
branches:
66
- $default-branch
7+
- default
8+
- 'docker/*'
79
tags:
810
- 'v*'
911
pull_request:

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
.PHONY: default
22
default: up
33

4+
.PHONY: help
5+
help:
6+
@echo "ACTIONS"
7+
@echo " up Bring up the docker env."
8+
@echo " down Bring down the docker env."
9+
@echo " build Build images associated with the services."
10+
@echo " logs View service logs."
11+
@echo " stat[us] List the status of the docker services."
12+
@echo " localup Bring up the local production env."
13+
@echo " localdown Bring down the local production env."
14+
@echo " locallogs View local production logs."
15+
@echo " clean Clean up generated files."
16+
417
.PHONY: up
518
up:
619
docker-compose up --build -d

docker-compose.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,10 @@ services:
2121
build: ./api
2222
environment:
2323
APP_MODE: development
24-
# DB_DIALECT: postgres
25-
DB_DIALECT: mariadb
26-
# DB_HOST: db
27-
DB_HOST: mariadb
24+
DB_DIALECT: postgres
25+
DB_HOST: db
2826
DB_NAME: db
29-
# DB_USER: postgres
30-
DB_USER: user
27+
DB_USER: postgres
3128
DB_PASS: secret
3229
volumes:
3330
- ./api/src:/app/src:ro

utils/common/config.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import configparser
2+
import logging
3+
from pathlib import Path
4+
5+
6+
HERE = Path(__file__).parent.resolve()
7+
ROOT = Path(HERE)
8+
while not Path(ROOT, '.git').exists():
9+
ROOT = Path(ROOT, '..').resolve()
10+
11+
LOG_INIT = False
12+
13+
14+
class _Config:
15+
16+
FILES = [
17+
Path(Path.home(), '.config/hike-blog/config.ini'),
18+
Path(ROOT, '.hike-blog.ini'),
19+
]
20+
DEFAULTS = {
21+
'default': {},
22+
}
23+
24+
def __init__(self) -> None:
25+
self._conf = configparser.ConfigParser()
26+
self._conf.read_dict(self.DEFAULTS)
27+
self._conf.read(self.FILES)
28+
29+
def get(self, key: str, section: str = 'DEFAULT') -> str:
30+
''' Get a value. '''
31+
return self._conf[section][key]
32+
33+
34+
CONFIG = _Config()
35+
36+
37+
def get_logger() -> logging.Logger:
38+
global LOG_INIT
39+
40+
log = logging.getLogger('script')
41+
if not LOG_INIT:
42+
log.addHandler(logging.StreamHandler())
43+
log.setLevel(1)
44+
LOG_INIT = True
45+
46+
return log

utils/lint.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
3+
DEST_DIR="${1:-.}"
4+
OUTFILE="$DEST_DIR/results.txt"
5+
6+
if [[ -f $DEST_DIR ]]; then
7+
echo "Error: Provided destination is a file" >&2
8+
exit 1
9+
fi
10+
if [[ ! -d $DEST_DIR ]]; then
11+
echo "Error: Cannot find destination path '$DEST_DIR'" >&2
12+
exit 1
13+
fi
14+
15+
cd "$(dirname "$0")/.."
16+
17+
find api \
18+
-type f \
19+
-iname '*.py' \
20+
-exec \
21+
pylint \
22+
--output "${OUTFILE}" \
23+
--exit-zero \
24+
{} +
25+
echo $?
26+
echo ""
27+
cat results.txt

utils/prod.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
from datetime import datetime
5+
import io
6+
import itertools
7+
import json
8+
import os
9+
from pathlib import Path
10+
import re
11+
12+
from alembic import config
13+
import alembic
14+
import alembic.command
15+
import pytz
16+
import requests
17+
18+
from common.config import CONFIG, ROOT, get_logger
19+
20+
LOG = get_logger()
21+
22+
23+
####################################################################################################
24+
25+
def _time_type(value: str) -> datetime:
26+
dates = ['%Y-%m-%d', '%Y/%m/%d', '%b %m %Y']
27+
times = ['%H', '%H:%M', '%H:%M:%S']
28+
fmts = itertools.product(dates, times)
29+
fmts, t_variant = itertools.tee(fmts)
30+
fmts = map(' '.join, fmts)
31+
t_variant = map('T'.join, t_variant)
32+
fmts = list(itertools.chain(fmts, t_variant))
33+
try:
34+
return datetime.fromisoformat(value)
35+
except ValueError:
36+
pass
37+
for fmt in fmts:
38+
try:
39+
return datetime.strptime(value, fmt)
40+
except ValueError:
41+
pass
42+
raise argparse.ArgumentTypeError(f'Unable to resolve {value} into a datetime')
43+
44+
45+
def _tz_type(value: str) -> str:
46+
if value not in pytz.common_timezones:
47+
raise argparse.ArgumentTypeError(f'Unknown timezone string "{value}"')
48+
return value
49+
50+
51+
####################################################################################################
52+
53+
def _action_migrate(args_):
54+
'''
55+
Migrate the production database. If nothing is provided, upgrade to "head" is assumed;
56+
otherwise, "base" can be used to completely revert the migrations, or a relative identifier can
57+
be use (e.g. -3 or +2).
58+
'''
59+
parser = argparse.ArgumentParser()
60+
parser.add_argument('rev', nargs='?', default='head')
61+
args = parser.parse_args(args_.others)
62+
63+
up_pat = re.compile(r'(head|\+\d+)')
64+
down_pat = re.compile(r'(base|-\d+)')
65+
66+
if up_pat.match(args.rev):
67+
direction = 'upgrade'
68+
elif down_pat.match(args.rev):
69+
direction = 'downgrade'
70+
else:
71+
raise ValueError(f'Unable to interpret revision "{args.rev}"')
72+
73+
api_dir = Path(ROOT, 'api')
74+
75+
aconf = config.Config(str(Path(api_dir, 'alembic.ini')))
76+
aconf.set_section_option('alembic', 'sqlalchemy.url', CONFIG.get('uri', 'database'))
77+
78+
os.chdir(api_dir)
79+
print(f'Direction: {direction}')
80+
if direction == 'upgrade':
81+
alembic.command.upgrade(aconf, args.rev)
82+
elif direction == 'downgrade':
83+
alembic.command.downgrade(aconf, args.rev)
84+
else:
85+
raise RuntimeError('Unknown state')
86+
87+
88+
def _action_list(args_):
89+
parser = argparse.ArgumentParser()
90+
args = parser.parse_args(args_.others)
91+
92+
base_url = CONFIG.get('url', section='app')
93+
url = f'{base_url}/api/hikes'
94+
resp = requests.get(url)
95+
resp.raise_for_status()
96+
97+
data = resp.json()['data']
98+
data = map(json.dumps, data)
99+
data = map(print, data)
100+
data = list(data)
101+
102+
103+
def _action_create(args_):
104+
''' Create a new hike. '''
105+
parser = argparse.ArgumentParser()
106+
parser.add_argument('name')
107+
parser.add_argument('--title')
108+
parser.add_argument('--brief')
109+
parser.add_argument('--description')
110+
parser.add_argument('--start', type=_time_type)
111+
parser.add_argument('--end', type=_time_type)
112+
parser.add_argument('--timezone', type=_tz_type, default='UTC')
113+
args = parser.parse_args(args_.others)
114+
115+
base_url = CONFIG.get('url', section='app')
116+
api_key = CONFIG.get('api-key', section='app')
117+
url = f'{base_url}/api/hikes/new'
118+
119+
data = {
120+
'name': args.name,
121+
'title': args.title,
122+
'brief': args.brief,
123+
'description': args.description,
124+
'start': args.start.isoformat() if args.start else args.start,
125+
'end': args.end.isoformat() if args.end else args.end,
126+
'zone': args.timezone,
127+
}
128+
headers = {
129+
'Api-Session': api_key,
130+
}
131+
132+
data = data.items()
133+
data = filter(lambda x: x[1] is not None, data)
134+
data = dict(data)
135+
136+
resp = requests.post(url, json=data, headers=headers, timeout=10)
137+
resp.raise_for_status()
138+
139+
print(' Hike: {json.dumps(resp.json(), indent=2)}')
140+
141+
142+
def _action_update(args_):
143+
''' Update hike info. '''
144+
parser = argparse.ArgumentParser()
145+
parser.add_argument('hikeid', type=int)
146+
parser.add_argument('--name')
147+
parser.add_argument('--title')
148+
parser.add_argument('--brief')
149+
parser.add_argument('--description')
150+
parser.add_argument('--start', type=_time_type)
151+
parser.add_argument('--end', type=_time_type)
152+
parser.add_argument('--timezone', type=_tz_type)
153+
args = parser.parse_args(args_.others)
154+
155+
156+
base_url = CONFIG.get('url', section='app')
157+
api_key = CONFIG.get('api-key', section='app')
158+
url = f'{base_url}/api/hikes/{args.hikeid}'
159+
160+
data = {
161+
'name': args.name,
162+
'title': args.title,
163+
'brief': args.brief,
164+
'description': args.description,
165+
'start': args.start.isoformat() if args.start else args.start,
166+
'end': args.end.isoformat() if args.end else args.end,
167+
'zone': args.timezone,
168+
}
169+
headers = {
170+
'Api-Session': api_key,
171+
}
172+
173+
data = data.items()
174+
data = filter(lambda x: x[1] is not None, data)
175+
data = dict(data)
176+
177+
resp = requests.post(url, json=data, headers=headers, timeout=10)
178+
resp.raise_for_status()
179+
180+
print(' Hike: {json.dumps(resp.json(), indent=2)}')
181+
182+
183+
def _action_upload(args_):
184+
''' Upload pictures and GPX files to hike. '''
185+
parser = argparse.ArgumentParser()
186+
parser.add_argument('hikeid', type=int)
187+
parser.add_argument('files', nargs='+', type=Path)
188+
args = parser.parse_args(args_.others)
189+
190+
pic_files = ('.jpg', '.jpeg')
191+
data_files = ('.gpx',)
192+
193+
for file in args.files:
194+
if file.suffix.lower() not in (*pic_files, *data_files):
195+
raise ValueError(f'Unhandlable file type {file}')
196+
197+
base_url = CONFIG.get('url', section='app')
198+
api_key = CONFIG.get('api-key', section='app')
199+
headers = {'Api-Session': api_key}
200+
201+
for file in args.files:
202+
if file.suffix.lower() in pic_files:
203+
url = f'{base_url}/api/pictures/hike/{args.hikeid}'
204+
else:
205+
url = f'{base_url}/api/hikes/{args.hikeid}/data'
206+
with open(file, 'rb') as inf:
207+
files = {file.name: inf}
208+
resp = requests.post(
209+
url,
210+
files=files,
211+
headers=headers,
212+
timeout=30,
213+
)
214+
resp.raise_for_status()
215+
216+
217+
####################################################################################################
218+
219+
def _main():
220+
actions = {
221+
'migrate': _action_migrate,
222+
'list': _action_list,
223+
'create': _action_create,
224+
'update': _action_update,
225+
'upload': _action_upload,
226+
}
227+
228+
parser = argparse.ArgumentParser()
229+
parser.add_argument(
230+
'action',
231+
choices=list(actions.keys()),
232+
)
233+
parser.add_argument(
234+
'others',
235+
nargs=argparse.REMAINDER,
236+
)
237+
args = parser.parse_args()
238+
239+
actions[args.action](args)
240+
241+
242+
if __name__ == '__main__':
243+
_main()

0 commit comments

Comments
 (0)