Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"packageRules": [
{
"matchManagers": ["pip_requirements"],
"matchManagers": ["pep621"],
"enabled": true
}
],
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ETC
.bash_history
.serena/
*.db
*.key
*.pem
Expand All @@ -8,6 +9,7 @@
*.sqlite
*cache
*report.json
CLAUDE.md
gen_token.py
gitleaks_report*.json
raw/
Expand Down
6 changes: 3 additions & 3 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
nodejs 24.2.0
nodejs 24.5.0
python 3.11.13
ruby 3.4.4
uv 0.7.3
ruby 3.4.5
uv 0.8.8
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"recommendations": [
"aaron-bond.better-comments",
"codezombiech.gitignore",
"eamodio.gitlens",
"EditorConfig.EditorConfig",
Expand Down
70 changes: 70 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# meetup_bot Project Reference

## General Instructions

- Minimize inline comments
- Retain tabs, spaces, and encoding
- Fix linting errors before saving files.
- Respect `.markdownlint.jsonc` rules for all markdown files
- If under 50 lines of code (LOC), print the full function or class
- If the token limit is close or it's over 50 LOC, print the line numbers and avoid comments altogether
- Explain as much as possible in the chat unless asked to annotate (i.e., docstrings, newline comments, etc.)

## Build, Lint, and Test Commands

- Full test suite: `uv run pytest` or `task test`
- Single test: `uv run pytest tests/test_filename.py::test_function_name`
- Linting: `uv run ruff check --fix --respect-gitignore` or `task lint`
- Formatting: `uv run ruff format --respect-gitignore` or `task format`
- Check dependencies: `uv run deptry .` or `task deptry`
- Pre-commit hooks: `pre-commit run --all-files` or `task pre-commit`

## Code Style Guidelines

- **Formatting**: 4 spaces, 130-char line limit, LF line endings
- **Imports**: Ordered by type, combined imports when possible
- **Naming**: snake_case functions/vars, PascalCase classes, UPPERCASE constants
- **Type Hints**: Use Optional for nullable params, pipe syntax for Union
- **Error Handling**: Specific exception types, descriptive error messages
- **File Structure**: Core logic in app/core/, utilities in app/utils/
- **Docstrings**: Use double quotes for docstrings
- **Tests**: Files in tests/, follow test_* naming convention

## GraphQL API Troubleshooting

When debugging GraphQL API issues (particularly for Meetup API):

### 1. Direct GraphQL Testing
- Test queries directly against the GraphQL endpoint using curl before debugging application code
- 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 } }"}'`
- Start with simple queries (like `self { id name }`) then gradually add complexity

### 2. API Migration Validation
- Check API documentation for migration guides when encountering field errors
- Common Meetup API changes:
- `count` → `totalCount`
- `upcomingEvents` → `memberEvents(first: N)` for self queries
- `upcomingEvents` → `events(first: N)` for group queries
- Syntax changes: `field(input: {first: N})` → `field(first: N)`

### 3. Response Structure Analysis
- Add temporary debug logging to inspect actual GraphQL responses
- Check for `errors` array in GraphQL responses, not just HTTP status codes
- Verify field existence with introspection or simple field queries
- Example debug pattern:
```python
response_data = r.json()
if 'errors' in response_data:
print('GraphQL Errors:', json.dumps(response_data['errors'], indent=2))
```

### 4. Field Validation Process
- Use GraphQL validation errors to identify undefined fields
- Test field names individually: `{ self { fieldName } }`
- Check if field requires parameters (e.g., `memberEvents` requires `first`)
- Validate nested field access patterns

### 5. Token and Authentication Debugging
- Verify token generation is working: `uv run python -c "from app.sign_jwt import main; print(main())"`
- Test tokens directly against GraphQL endpoint outside of application
- Check token expiration and refresh token logic
31 changes: 0 additions & 31 deletions CLAUDE.md

This file was deleted.

6 changes: 3 additions & 3 deletions app/capture_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
base_url = "https://www.meetup.com"
# * # anyDistance (default), twoMiles, fiveMiles, tenMiles, twentyFiveMiles, fiftyMiles, hundredMiles
distance = "tenMiles"
source = "GROUPS" # EVENTS (default), GROUPS
category_id = "546" # technology groups
location = "us--ok--Oklahoma%20City" # OKC
source = "GROUPS" # EVENTS (default), GROUPS
category_id = "546" # technology groups
location = "us--ok--Oklahoma%20City" # OKC

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

Expand Down
33 changes: 15 additions & 18 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,13 @@ class UserInfo(db.Entity):
DB_PASS = DB_PASS.strip('"')

# postgres db
db.bind(provider='postgres',
user=DB_USER,
password=DB_PASS,
host=DB_HOST,
database=DB_NAME,
port=DB_PORT,
db.bind(
provider='postgres',
user=DB_USER,
password=DB_PASS,
host=DB_HOST,
database=DB_NAME,
port=DB_PORT,
)

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

# TODO: decouple export from formatted response
@api_router.get("/events")
def get_events(auth: dict = Depends(ip_whitelist_or_auth),
location: str = "Oklahoma City",
exclusions: str = "Tulsa",
current_user: User = Depends(get_current_active_user)
):
def get_events(
auth: dict = Depends(ip_whitelist_or_auth),
location: str = "Oklahoma City",
exclusions: str = "Tulsa",
current_user: User = Depends(get_current_active_user),
):
"""
Query upcoming Meetup events

Expand Down Expand Up @@ -419,7 +421,7 @@ def get_events(auth: dict = Depends(ip_whitelist_or_auth),
if not os.path.exists(json_fn) or os.stat(json_fn).st_size == 0:
return {"message": "No events found", "events": []}

return pd.read_json(json_fn)
return pd.read_json(json_fn).to_dict('records')


@api_router.get("/check-schedule")
Expand Down Expand Up @@ -584,12 +586,7 @@ def main():
import uvicorn

try:
uvicorn.run("main:app",
host="0.0.0.0",
port=PORT,
limit_max_requests=10000,
log_level="warning",
reload=True)
uvicorn.run("main:app", host="0.0.0.0", port=PORT, limit_max_requests=10000, log_level="warning", reload=True)
except KeyboardInterrupt:
print("\nExiting...")
sys.exit(0)
Expand Down
64 changes: 41 additions & 23 deletions app/meetup_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
name
username
memberUrl
upcomingEvents {
count
memberEvents(first: 10) {
totalCount
pageInfo {
endCursor
}
Expand Down Expand Up @@ -112,8 +112,8 @@
urlname
city
link
upcomingEvents(input: { first: 1 }) {
count
events(first: 10) {
totalCount
pageInfo {
endCursor
}
Expand Down Expand Up @@ -143,15 +143,21 @@ def send_request(token, query, vars) -> str:
"""
Request

POST https://api.meetup.com/gql
POST https://api.meetup.com/gql-ext
"""

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

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

try:
r = requests.post(endpoint, json={'query': query, 'variables': vars}, headers=headers)
# Parse vars string to JSON object if it's a string
if isinstance(vars, str):
variables = json.loads(vars)
else:
variables = vars

r = requests.post(endpoint, json={'query': query, 'variables': variables}, headers=headers)
print(f"{Fore.GREEN}{info:<10}{Fore.RESET}Response HTTP Response Body: {r.status_code}")

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

# TODO: add arg for `self` or `groupByUrlname`
# extract data from json
try:
data = response_json['data']['self']['upcomingEvents']['edges']
if data[0]['node']['group']['city'] != location:
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping event outside of {location}")
except KeyError:
if response_json['data']['groupByUrlname'] is None:
data = ""
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping group due to empty response")
pass
else:
data = response_json['data']['groupByUrlname']['upcomingEvents']['edges']
# TODO: handle no upcoming events to fallback on initial response
if response_json['data']['groupByUrlname']['city'] != location:
print(f"{Fore.RED}{error:<10}{Fore.RESET}No data for {location} found")
pass
data = None

# Check if response has expected structure
if 'data' not in response_json:
print(
f"{Fore.RED}{error:<10}{Fore.RESET}GraphQL response missing 'data' key. Response: {json.dumps(response_json, indent=2)[:500]}"
)
data = ""
else:
try:
data = response_json['data']['self']['memberEvents']['edges']
if data and len(data) > 0 and data[0]['node']['group']['city'] != location:
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping event outside of {location}")
except KeyError:
try:
if response_json['data'].get('groupByUrlname') is None:
data = ""
print(f"{Fore.YELLOW}{warning:<10}{Fore.RESET}Skipping group due to empty response")
else:
data = response_json['data']['groupByUrlname']['events']['edges']
# TODO: handle no upcoming events to fallback on initial response
if response_json['data']['groupByUrlname']['city'] != location:
print(f"{Fore.RED}{error:<10}{Fore.RESET}No data for {location} found")
except KeyError as e:
print(f"{Fore.RED}{error:<10}{Fore.RESET}KeyError accessing GraphQL data: {e}")
print(f"{Fore.RED}{error:<10}{Fore.RESET}Response structure: {json.dumps(response_json, indent=2)[:500]}")
data = ""

# append data to rows
if data is not None:
if data:
for i in range(len(data)):
df.loc[i, 'name'] = data[i]['node']['group']['name']
df.loc[i, 'date'] = data[i]['node']['dateTime']
Expand Down
4 changes: 4 additions & 0 deletions app/slackbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ def fmt_json(filename):
# create dataframe
df = pd.DataFrame(data)

# handle empty dataframe case
if df.empty:
return []

# add column: 'message' with date, name, title, eventUrl
df['message'] = df.apply(lambda x: f'• {x["date"]} *{x["name"]}* <{x["eventUrl"]}|{x["title"]}> ', axis=1)

Expand Down
Loading