Skip to content

Commit 3b48647

Browse files
Merge pull request #177 from pythoninthegrass/dev
fix: pandas formatting
2 parents f87f55e + 9f7c5ce commit 3b48647

File tree

13 files changed

+230
-326
lines changed

13 files changed

+230
-326
lines changed

.github/renovate.json5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"packageRules": [
99
{
10-
"matchManagers": ["pip_requirements"],
10+
"matchManagers": ["pep621"],
1111
"enabled": true
1212
}
1313
],

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# ETC
22
.bash_history
3+
.serena/
34
*.db
45
*.key
56
*.pem
@@ -8,6 +9,7 @@
89
*.sqlite
910
*cache
1011
*report.json
12+
CLAUDE.md
1113
gen_token.py
1214
gitleaks_report*.json
1315
raw/

.tool-versions

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
nodejs 24.2.0
1+
nodejs 24.5.0
22
python 3.11.13
3-
ruby 3.4.4
4-
uv 0.7.3
3+
ruby 3.4.5
4+
uv 0.8.8

.vscode/extensions.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"recommendations": [
3-
"aaron-bond.better-comments",
43
"codezombiech.gitignore",
54
"eamodio.gitlens",
65
"EditorConfig.EditorConfig",

AGENTS.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# meetup_bot Project Reference
2+
3+
## General Instructions
4+
5+
- Minimize inline comments
6+
- Retain tabs, spaces, and encoding
7+
- Fix linting errors before saving files.
8+
- Respect `.markdownlint.jsonc` rules for all markdown files
9+
- If under 50 lines of code (LOC), print the full function or class
10+
- If the token limit is close or it's over 50 LOC, print the line numbers and avoid comments altogether
11+
- Explain as much as possible in the chat unless asked to annotate (i.e., docstrings, newline comments, etc.)
12+
13+
## Build, Lint, and Test Commands
14+
15+
- Full test suite: `uv run pytest` or `task test`
16+
- Single test: `uv run pytest tests/test_filename.py::test_function_name`
17+
- Linting: `uv run ruff check --fix --respect-gitignore` or `task lint`
18+
- Formatting: `uv run ruff format --respect-gitignore` or `task format`
19+
- Check dependencies: `uv run deptry .` or `task deptry`
20+
- Pre-commit hooks: `pre-commit run --all-files` or `task pre-commit`
21+
22+
## Code Style Guidelines
23+
24+
- **Formatting**: 4 spaces, 130-char line limit, LF line endings
25+
- **Imports**: Ordered by type, combined imports when possible
26+
- **Naming**: snake_case functions/vars, PascalCase classes, UPPERCASE constants
27+
- **Type Hints**: Use Optional for nullable params, pipe syntax for Union
28+
- **Error Handling**: Specific exception types, descriptive error messages
29+
- **File Structure**: Core logic in app/core/, utilities in app/utils/
30+
- **Docstrings**: Use double quotes for docstrings
31+
- **Tests**: Files in tests/, follow test_* naming convention
32+
33+
## GraphQL API Troubleshooting
34+
35+
When debugging GraphQL API issues (particularly for Meetup API):
36+
37+
### 1. Direct GraphQL Testing
38+
- Test queries directly against the GraphQL endpoint using curl before debugging application code
39+
- Example: `curl -X POST "https://api.meetup.com/gql-ext" -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"query": "query { self { id name } }"}'`
40+
- Start with simple queries (like `self { id name }`) then gradually add complexity
41+
42+
### 2. API Migration Validation
43+
- Check API documentation for migration guides when encountering field errors
44+
- Common Meetup API changes:
45+
- `count``totalCount`
46+
- `upcomingEvents``memberEvents(first: N)` for self queries
47+
- `upcomingEvents``events(first: N)` for group queries
48+
- Syntax changes: `field(input: {first: N})``field(first: N)`
49+
50+
### 3. Response Structure Analysis
51+
- Add temporary debug logging to inspect actual GraphQL responses
52+
- Check for `errors` array in GraphQL responses, not just HTTP status codes
53+
- Verify field existence with introspection or simple field queries
54+
- Example debug pattern:
55+
```python
56+
response_data = r.json()
57+
if 'errors' in response_data:
58+
print('GraphQL Errors:', json.dumps(response_data['errors'], indent=2))
59+
```
60+
61+
### 4. Field Validation Process
62+
- Use GraphQL validation errors to identify undefined fields
63+
- Test field names individually: `{ self { fieldName } }`
64+
- Check if field requires parameters (e.g., `memberEvents` requires `first`)
65+
- Validate nested field access patterns
66+
67+
### 5. Token and Authentication Debugging
68+
- Verify token generation is working: `uv run python -c "from app.sign_jwt import main; print(main())"`
69+
- Test tokens directly against GraphQL endpoint outside of application
70+
- Check token expiration and refresh token logic

CLAUDE.md

Lines changed: 0 additions & 31 deletions
This file was deleted.

app/capture_groups.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
base_url = "https://www.meetup.com"
2323
# * # anyDistance (default), twoMiles, fiveMiles, tenMiles, twentyFiveMiles, fiftyMiles, hundredMiles
2424
distance = "tenMiles"
25-
source = "GROUPS" # EVENTS (default), GROUPS
26-
category_id = "546" # technology groups
27-
location = "us--ok--Oklahoma%20City" # OKC
25+
source = "GROUPS" # EVENTS (default), GROUPS
26+
category_id = "546" # technology groups
27+
location = "us--ok--Oklahoma%20City" # OKC
2828

2929
url = base_url + "/find/?distance=" + distance + "&source=" + source + "&categoryId=" + category_id + "&location=" + location
3030

app/main.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,13 @@ class UserInfo(db.Entity):
135135
DB_PASS = DB_PASS.strip('"')
136136

137137
# postgres db
138-
db.bind(provider='postgres',
139-
user=DB_USER,
140-
password=DB_PASS,
141-
host=DB_HOST,
142-
database=DB_NAME,
143-
port=DB_PORT,
138+
db.bind(
139+
provider='postgres',
140+
user=DB_USER,
141+
password=DB_PASS,
142+
host=DB_HOST,
143+
database=DB_NAME,
144+
port=DB_PORT,
144145
)
145146

146147
# generate mapping
@@ -364,11 +365,12 @@ def generate_token(current_user: User = Depends(get_current_active_user)):
364365

365366
# TODO: decouple export from formatted response
366367
@api_router.get("/events")
367-
def get_events(auth: dict = Depends(ip_whitelist_or_auth),
368-
location: str = "Oklahoma City",
369-
exclusions: str = "Tulsa",
370-
current_user: User = Depends(get_current_active_user)
371-
):
368+
def get_events(
369+
auth: dict = Depends(ip_whitelist_or_auth),
370+
location: str = "Oklahoma City",
371+
exclusions: str = "Tulsa",
372+
current_user: User = Depends(get_current_active_user),
373+
):
372374
"""
373375
Query upcoming Meetup events
374376
@@ -419,7 +421,7 @@ def get_events(auth: dict = Depends(ip_whitelist_or_auth),
419421
if not os.path.exists(json_fn) or os.stat(json_fn).st_size == 0:
420422
return {"message": "No events found", "events": []}
421423

422-
return pd.read_json(json_fn)
424+
return pd.read_json(json_fn).to_dict('records')
423425

424426

425427
@api_router.get("/check-schedule")
@@ -584,12 +586,7 @@ def main():
584586
import uvicorn
585587

586588
try:
587-
uvicorn.run("main:app",
588-
host="0.0.0.0",
589-
port=PORT,
590-
limit_max_requests=10000,
591-
log_level="warning",
592-
reload=True)
589+
uvicorn.run("main:app", host="0.0.0.0", port=PORT, limit_max_requests=10000, log_level="warning", reload=True)
593590
except KeyboardInterrupt:
594591
print("\nExiting...")
595592
sys.exit(0)

app/meetup_query.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@
7474
name
7575
username
7676
memberUrl
77-
upcomingEvents {
78-
count
77+
memberEvents(first: 10) {
78+
totalCount
7979
pageInfo {
8080
endCursor
8181
}
@@ -112,8 +112,8 @@
112112
urlname
113113
city
114114
link
115-
upcomingEvents(input: { first: 1 }) {
116-
count
115+
events(first: 10) {
116+
totalCount
117117
pageInfo {
118118
endCursor
119119
}
@@ -143,15 +143,21 @@ def send_request(token, query, vars) -> str:
143143
"""
144144
Request
145145
146-
POST https://api.meetup.com/gql
146+
POST https://api.meetup.com/gql-ext
147147
"""
148148

149-
endpoint = 'https://api.meetup.com/gql'
149+
endpoint = 'https://api.meetup.com/gql-ext'
150150

151151
headers = {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json; charset=utf-8'}
152152

153153
try:
154-
r = requests.post(endpoint, json={'query': query, 'variables': vars}, headers=headers)
154+
# Parse vars string to JSON object if it's a string
155+
if isinstance(vars, str):
156+
variables = json.loads(vars)
157+
else:
158+
variables = vars
159+
160+
r = requests.post(endpoint, json={'query': query, 'variables': variables}, headers=headers)
155161
print(f"{Fore.GREEN}{info:<10}{Fore.RESET}Response HTTP Response Body: {r.status_code}")
156162

157163
# pretty prints json response content but skips sorting keys as it rearranges graphql response
@@ -180,24 +186,36 @@ def format_response(response, location: str = "Oklahoma City", exclusions: str =
180186

181187
# TODO: add arg for `self` or `groupByUrlname`
182188
# extract data from json
183-
try:
184-
data = response_json['data']['self']['upcomingEvents']['edges']
185-
if data[0]['node']['group']['city'] != location:
186-
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping event outside of {location}")
187-
except KeyError:
188-
if response_json['data']['groupByUrlname'] is None:
189-
data = ""
190-
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping group due to empty response")
191-
pass
192-
else:
193-
data = response_json['data']['groupByUrlname']['upcomingEvents']['edges']
194-
# TODO: handle no upcoming events to fallback on initial response
195-
if response_json['data']['groupByUrlname']['city'] != location:
196-
print(f"{Fore.RED}{error:<10}{Fore.RESET}No data for {location} found")
197-
pass
189+
data = None
190+
191+
# Check if response has expected structure
192+
if 'data' not in response_json:
193+
print(
194+
f"{Fore.RED}{error:<10}{Fore.RESET}GraphQL response missing 'data' key. Response: {json.dumps(response_json, indent=2)[:500]}"
195+
)
196+
data = ""
197+
else:
198+
try:
199+
data = response_json['data']['self']['memberEvents']['edges']
200+
if data and len(data) > 0 and data[0]['node']['group']['city'] != location:
201+
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping event outside of {location}")
202+
except KeyError:
203+
try:
204+
if response_json['data'].get('groupByUrlname') is None:
205+
data = ""
206+
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping group due to empty response")
207+
else:
208+
data = response_json['data']['groupByUrlname']['events']['edges']
209+
# TODO: handle no upcoming events to fallback on initial response
210+
if response_json['data']['groupByUrlname']['city'] != location:
211+
print(f"{Fore.RED}{error:<10}{Fore.RESET}No data for {location} found")
212+
except KeyError as e:
213+
print(f"{Fore.RED}{error:<10}{Fore.RESET}KeyError accessing GraphQL data: {e}")
214+
print(f"{Fore.RED}{error:<10}{Fore.RESET}Response structure: {json.dumps(response_json, indent=2)[:500]}")
215+
data = ""
198216

199217
# append data to rows
200-
if data is not None:
218+
if data:
201219
for i in range(len(data)):
202220
df.loc[i, 'name'] = data[i]['node']['group']['name']
203221
df.loc[i, 'date'] = data[i]['node']['dateTime']

app/slackbot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ def fmt_json(filename):
7676
# create dataframe
7777
df = pd.DataFrame(data)
7878

79+
# handle empty dataframe case
80+
if df.empty:
81+
return []
82+
7983
# add column: 'message' with date, name, title, eventUrl
8084
df['message'] = df.apply(lambda x: f'• {x["date"]} *{x["name"]}* <{x["eventUrl"]}|{x["title"]}> ', axis=1)
8185

0 commit comments

Comments
 (0)