Skip to content

Commit f87f55e

Browse files
Merge remote-tracking branch 'origin/dev'
2 parents f9810df + bf59425 commit f87f55e

File tree

11 files changed

+1665
-133
lines changed

11 files changed

+1665
-133
lines changed

.github/workflows/hooks/pre-commit.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env python
22

33
import os
4-
import sys
54
import subprocess
5+
import sys
66

77

88
def gitleaksEnabled():
@@ -23,5 +23,7 @@ def gitleaksEnabled():
2323
''')
2424
sys.exit(1)
2525
else:
26-
print('gitleaks precommit disabled\
27-
(enable with `git config hooks.gitleaks true`)')
26+
print(
27+
'gitleaks precommit disabled\
28+
(enable with `git config hooks.gitleaks true`)'
29+
)

app/main.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22

33
import arrow
4+
import os
45
import pandas as pd
56
import sys
67
import time
@@ -18,9 +19,9 @@
1819
from meetup_query import *
1920
from passlib.context import CryptContext
2021
from pathlib import Path
21-
from pony.orm import Database, Required, Optional, PrimaryKey, Set, db_session
22+
from pony.orm import Database, Optional, PrimaryKey, Required, Set, db_session
2223
from pydantic import BaseModel
23-
from schedule import get_schedule, get_current_schedule_time, snooze_schedule, check_and_revert_snooze
24+
from schedule import check_and_revert_snooze, get_current_schedule_time, get_schedule, snooze_schedule
2425
from sign_jwt import main as gen_token
2526
from slackbot import *
2627
from typing import List, Union
@@ -74,8 +75,8 @@
7475

7576

7677
class IPConfig(BaseModel):
77-
whitelist: List[str] = ["localhost", "127.0.0.1"]
78-
public_ips: List[str] = [] # TODO: add whitelisted public IPs here
78+
whitelist: list[str] = ["localhost", "127.0.0.1"]
79+
public_ips: list[str] = [] # TODO: add whitelisted public IPs here
7980

8081

8182
ip_config = IPConfig()
@@ -254,7 +255,7 @@ async def ip_whitelist_or_auth(request: Request, current_user: User = Depends(ge
254255
return current_user
255256

256257

257-
def check_auth(auth: Union[dict, User]) -> None:
258+
def check_auth(auth: dict | User) -> None:
258259
"""
259260
Shared function to check authentication result.
260261
Raises an HTTPException if authentication fails.
@@ -276,9 +277,7 @@ async def login_for_oauth_token(form_data: OAuth2PasswordRequestForm = Depends()
276277
headers={"WWW-Authenticate": "Bearer"},
277278
)
278279
oauth_token_expires = timedelta(minutes=TOKEN_EXPIRE)
279-
oauth_token = create_access_token(
280-
data={"sub": user.username}, expires_delta=oauth_token_expires
281-
)
280+
oauth_token = create_access_token(data={"sub": user.username}, expires_delta=oauth_token_expires)
282281

283282
return {"access_token": oauth_token, "token_type": "bearer"}
284283

@@ -416,6 +415,10 @@ def get_events(auth: dict = Depends(ip_whitelist_or_auth),
416415
# cleanup output file
417416
sort_json(json_fn)
418417

418+
# check if file exists after sorting
419+
if not os.path.exists(json_fn) or os.stat(json_fn).st_size == 0:
420+
return {"message": "No events found", "events": []}
421+
419422
return pd.read_json(json_fn)
420423

421424

@@ -524,7 +527,7 @@ def post_slack(
524527
def snooze_slack_post(
525528
duration: str,
526529
auth: dict = Depends(ip_whitelist_or_auth),
527-
):
530+
):
528531
"""
529532
Snooze the Slack post for the specified duration
530533
@@ -544,7 +547,7 @@ def snooze_slack_post(
544547

545548
# TODO: test IP whitelisting
546549
@api_router.get("/schedule")
547-
def get_current_schedule(auth: Union[dict, User] = Depends(ip_whitelist_or_auth)):
550+
def get_current_schedule(auth: dict | User = Depends(ip_whitelist_or_auth)):
548551
"""
549552
Get the current schedule including any active snoozes
550553
"""

app/meetup_query.py

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from colorama import Fore
1212
from decouple import config
1313
from icecream import ic
14-
from sign_jwt import main as gen_token
1514
from pathlib import Path
15+
from sign_jwt import main as gen_token
1616

1717
# verbose icecream
1818
ic.configureOutput(includeContext=True)
@@ -45,8 +45,8 @@
4545
raise FileNotFoundError(f"groups.csv not found in {script_dir} or {cwd}")
4646

4747
# time span (e.g., 3600 = 1 hour)
48-
sec = int(60) # n seconds
49-
ttl = int(sec * 30) # n minutes -> hours
48+
sec = 60 # n seconds
49+
ttl = int(sec * 30) # n minutes -> hours
5050

5151
# cache the requests as script basename, expire after n time
5252
requests_cache.install_cache(Path(cache_fn), expire_after=ttl)
@@ -148,17 +148,10 @@ def send_request(token, query, vars) -> str:
148148

149149
endpoint = 'https://api.meetup.com/gql'
150150

151-
headers = {
152-
'Authorization': f'Bearer {token}',
153-
'Content-Type': 'application/json; charset=utf-8'
154-
}
151+
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json; charset=utf-8'}
155152

156153
try:
157-
r = requests.post(
158-
endpoint,
159-
json={'query': query, 'variables': vars},
160-
headers=headers
161-
)
154+
r = requests.post(endpoint, json={'query': query, 'variables': vars}, headers=headers)
162155
print(f"{Fore.GREEN}{info:<10}{Fore.RESET}Response HTTP Response Body: {r.status_code}")
163156

164157
# pretty prints json response content but skips sorting keys as it rearranges graphql response
@@ -167,7 +160,7 @@ def send_request(token, query, vars) -> str:
167160
# formatted response
168161
# print('Response HTTP Response Body:\n{content}'.format(content=pretty_response))
169162
except requests.exceptions.RequestException as e:
170-
print('HTTP Request failed:\n{error}'.format(error=e))
163+
print(f'HTTP Request failed:\n{e}')
171164
sys.exit(1)
172165

173166
return pretty_response
@@ -303,7 +296,7 @@ def sort_json(filename) -> None:
303296
pass
304297

305298
# control for timestamp edge case `1-07-21 18:00:00` || `1-01-25 10:00:00` raising OutOfBoundsError
306-
df['date'] = pd.to_datetime(df['date'], errors='coerce')
299+
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%dT%H:%M:%S', errors='coerce')
307300

308301
# convert datetimeindex to datetime
309302
df['date'] = df['date'].dt.tz_localize(None)
@@ -327,7 +320,7 @@ def sort_json(filename) -> None:
327320
json.dump(data, f, indent=2)
328321

329322

330-
def export_to_file(response, type: str='json', exclusions: str='') -> None:
323+
def export_to_file(response, type: str = 'json', exclusions: str = '') -> None:
331324
"""
332325
Export to CSV or JSON
333326
"""
@@ -361,7 +354,7 @@ def export_to_file(response, type: str='json', exclusions: str='') -> None:
361354
and os.stat(json_fn).st_size > 0
362355
):
363356
# append to json
364-
with open(json_fn, 'r') as f:
357+
with open(json_fn) as f:
365358
data_json = json.load(f)
366359
data_json.extend(data)
367360
with open(json_fn, 'w', encoding='utf-8') as f:
@@ -393,7 +386,7 @@ def main():
393386
# first-party query
394387
response = send_request(access_token, query, vars)
395388
# format_response(response, exclusions=exclusions) # don't need if exporting to file
396-
export_to_file(response, format, exclusions=exclusions) # csv/json
389+
export_to_file(response, format, exclusions=exclusions) # csv/json
397390

398391
# third-party query
399392
output = []

app/schedule.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import time
55
from datetime import datetime, timedelta
66
from decouple import config
7-
from pony.orm import Database, Required, Optional, PrimaryKey, Set, db_session
7+
from pony.orm import Database, Optional, PrimaryKey, Required, Set, db_session
88

99
# env
1010
DB_NAME = config("DB_NAME")
1111
DB_USER = config("DB_USER")
1212
DB_PASS = config("DB_PASS")
1313
DB_HOST = config("DB_HOST")
1414
DB_PORT = config("DB_PORT", default=5432, cast=int)
15-
TZ = config("TZ", default="America/Chicago") # Set this to local timezone
15+
TZ = config("TZ", default="America/Chicago") # Set this to local timezone
1616
LOCAL_TIME = config("LOCAL_TIME", default="09:00") # Local time for schedule
1717

1818
# time

app/scheduler.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#!/usr/bin/env python3
22

3-
from distutils.log import warn
43
import arrow
54
import atexit
65
import os
@@ -10,6 +9,7 @@
109
from apscheduler.schedulers.background import BackgroundScheduler
1110
from colorama import Fore
1211
from decouple import config
12+
from distutils.log import warn
1313
from pathlib import Path
1414
from urllib.parse import urlencode
1515

@@ -48,9 +48,7 @@ def get_token():
4848

4949
payload = f"username={DB_USER}&password={DB_PASS}"
5050

51-
headers = {
52-
'Content-Type': 'application/x-www-form-urlencoded'
53-
}
51+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
5452

5553
try:
5654
res = requests.request("POST", url, headers=headers, data=payload)
@@ -63,7 +61,7 @@ def get_token():
6361

6462

6563
# @sched.scheduled_job(trigger='cron', hour='9,17,20,23', id='post_slack') # 9am, 5pm, 8pm, 11pm
66-
@sched.scheduled_job(trigger='cron', hour='*', id='post_slack') # every hour
64+
@sched.scheduled_job(trigger='cron', hour='*', id='post_slack') # every hour
6765
# @sched.scheduled_job(trigger='cron', minute='*/30', id='post_slack') # every n minutes
6866
def post_to_slack():
6967
"""Post to Slack"""
@@ -72,15 +70,9 @@ def post_to_slack():
7270

7371
url = f"http://{HOST}:{PORT}/api/slack"
7472

75-
payload = urlencode({
76-
'location': 'Oklahoma City',
77-
'exclusions': 'Tulsa'
78-
})
73+
payload = urlencode({'location': 'Oklahoma City', 'exclusions': 'Tulsa'})
7974

80-
headers = {
81-
'Authorization': f'Bearer {access_token}',
82-
'accept': 'application/json'
83-
}
75+
headers = {'Authorization': f'Bearer {access_token}', 'accept': 'application/json'}
8476

8577
try:
8678
res = requests.request("POST", url, headers=headers, data=payload)

app/sign_jwt.py

Lines changed: 13 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import sys
88
import time
99
from colorama import Fore
10-
from cryptography.hazmat.primitives import serialization
1110
from cryptography.hazmat.backends import default_backend
11+
from cryptography.hazmat.primitives import serialization
1212
from decouple import config
1313
from icecream import ic
1414
from pathlib import Path
@@ -42,42 +42,23 @@
4242
# load private key
4343
if isinstance(priv_key, pathlib.PosixPath) and priv_key.exists():
4444
with open(priv_key, 'rb') as f:
45-
private_key = serialization.load_pem_private_key(
46-
data=f.read(),
47-
password=None,
48-
backend=default_backend()
49-
)
45+
private_key = serialization.load_pem_private_key(data=f.read(), password=None, backend=default_backend())
5046
else:
5147
# decode base64
5248
private_key = base64.b64decode(priv_key)
5349
# load private key from env
54-
private_key = serialization.load_pem_private_key(
55-
data=private_key,
56-
password=None,
57-
backend=default_backend()
58-
)
50+
private_key = serialization.load_pem_private_key(data=private_key, password=None, backend=default_backend())
5951

6052
if isinstance(pub_key, pathlib.PosixPath) and pub_key.exists():
6153
with open(pub_key, 'rb') as f:
62-
public_key = serialization.load_pem_public_key(
63-
data=f.read(),
64-
backend=default_backend()
65-
)
54+
public_key = serialization.load_pem_public_key(data=f.read(), backend=default_backend())
6655
else:
6756
# decode base64
6857
public_key = base64.b64decode(pub_key)
6958
# load public key
70-
public_key = serialization.load_pem_public_key(
71-
data=public_key,
72-
backend=default_backend()
73-
)
59+
public_key = serialization.load_pem_public_key(data=public_key, backend=default_backend())
7460

75-
headers = {
76-
"alg": 'RS256',
77-
"typ": 'JWT',
78-
"Accept": 'application/json',
79-
"Content-Type": 'application/x-www-form-urlencoded'
80-
}
61+
headers = {"alg": 'RS256', "typ": 'JWT', "Accept": 'application/json', "Content-Type": 'application/x-www-form-urlencoded'}
8162

8263

8364
# TODO: Fix `Signature has expired\n[ERROR] Exception in ASGI application`; scheduler.sh only works for ~7 tries / 1 hour
@@ -87,33 +68,19 @@ def gen_payload_data():
8768
8869
Avoids `invalid_grant` by getting a new `exp` value during signing
8970
"""
90-
payload_data = {
91-
"sub": SELF_ID,
92-
"iss": CLIENT_ID,
93-
"aud": "api.meetup.com",
94-
"exp": int(time.time() + JWT_LIFE_SPAN)
95-
}
71+
payload_data = {"sub": SELF_ID, "iss": CLIENT_ID, "aud": "api.meetup.com", "exp": int(time.time() + JWT_LIFE_SPAN)}
9672
return payload_data
9773

9874

9975
def sign_token():
10076
"""Generate signed JWT"""
10177

10278
# Define headers exactly as specified in docs
103-
jwt_headers = {
104-
"kid": SIGNING_KEY_ID,
105-
"typ": "JWT",
106-
"alg": "RS256"
107-
}
79+
jwt_headers = {"kid": SIGNING_KEY_ID, "typ": "JWT", "alg": "RS256"}
10880

10981
payload_data = gen_payload_data()
11082

111-
payload = jwt.encode(
112-
headers=jwt_headers,
113-
payload=payload_data,
114-
key=private_key,
115-
algorithm='RS256'
116-
)
83+
payload = jwt.encode(headers=jwt_headers, payload=payload_data, key=private_key, algorithm='RS256')
11784

11885
return payload
11986

@@ -122,14 +89,7 @@ def verify_token(token):
12289
"""Verify signed JWT against public key"""
12390

12491
try:
125-
jwt.decode(
126-
jwt=token,
127-
key=public_key,
128-
issuer=CLIENT_ID,
129-
audience="api.meetup.com",
130-
verify=True,
131-
algorithms=['RS256']
132-
)
92+
jwt.decode(jwt=token, key=public_key, issuer=CLIENT_ID, audience="api.meetup.com", verify=True, algorithms=['RS256'])
13393
print(f"{Fore.GREEN}{info:<10}{Fore.RESET}Success! Token verified.")
13494
return True
13595
except jwt.exceptions.ExpiredSignatureError as e:
@@ -140,7 +100,7 @@ def verify_token(token):
140100
jwt.exceptions.InvalidSignatureError,
141101
jwt.exceptions.InvalidIssuerError,
142102
jwt.exceptions.InvalidAudienceError,
143-
) as e:
103+
) as e:
144104
print(f"{Fore.RED}{error:<10}{Fore.RESET}{e}")
145105
sys.exit(1)
146106

@@ -149,16 +109,11 @@ def get_access_token(token):
149109
"""Post token to auth server to get access token"""
150110

151111
# Headers for the token request
152-
request_headers = {
153-
"Content-Type": "application/x-www-form-urlencoded"
154-
}
112+
request_headers = {"Content-Type": "application/x-www-form-urlencoded"}
155113

156114
# Payload exactly as specified in docs
157115
# https://www.meetup.com/api/authentication/#p04-jwt-flow-section
158-
payload = {
159-
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
160-
"assertion": token
161-
}
116+
payload = {"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": token}
162117
payload = urlencode(payload)
163118

164119
try:

0 commit comments

Comments
 (0)