Skip to content

Commit cbe3a6a

Browse files
committed
Add guide to generate events using Meetup APIs
1 parent aa8c73d commit cbe3a6a

File tree

6 files changed

+221
-24
lines changed

6 files changed

+221
-24
lines changed

tools/events-automation/README.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

tools/events-automation/generate_events_meetup.py

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
from event import Event
1010

1111
def authenticate():
12+
"""
13+
Handles the OAuth 2.0 authentication process.
14+
Returns obtaining access and refresh tokens from the Meetup API
15+
"""
16+
# API Configuration:
1217
URL = "https://secure.meetup.com/oauth2/access"
1318
headers = {
1419
"Content-Type": "application/x-www-form-urlencoded"
@@ -17,6 +22,8 @@ def authenticate():
1722
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
1823
"assertion": generate_signed_jwt()
1924
}
25+
26+
# Make a request for access and refresh tokens
2027
response = requests.post(url=URL, headers=headers, data=body)
2128
if response.status_code == 200:
2229
access_token = response.json().get("access_token")
@@ -34,18 +41,29 @@ def authenticate():
3441
GEOLOCATOR = Nominatim(user_agent="TWiR")
3542

3643
def fetch_groups(endCursor=""):
44+
"""
45+
Returns the response from the API call, which includes data on groups matching the criteria specified in the GraphQL query.
46+
:type endCursor: An optional string parameter used for pagination, indicating the starting point of the query for fetching subsequent pages of results
47+
:rtype: requests.Response
48+
"""
49+
50+
# API Configuration:
51+
# Sets the API endpoint and constructs headers using an access token for authentication.
3752
URL = "https://api.meetup.com/gql"
3853
access_token, refresh_token = ACCESS_TOKEN, REFRESH_TOKEN
3954

4055
if not access_token:
4156
print("Authentication failed, cannot proceed to fetch events.")
4257
return
4358

59+
# Sets the content type to application/json for the request body.
4460
headers = {
4561
"Authorization": f"Bearer {access_token}",
4662
"Content-Type": "application/json",
4763
}
4864

65+
# GraphQL Query:
66+
# Below is a GraphQL query that requests information about groups such as ID, name, link, URL name, latitude, and longitude.
4967
data = {
5068
"query": """
5169
query (
@@ -79,6 +97,7 @@ def fetch_groups(endCursor=""):
7997
}
8098
}
8199
""",
100+
# The query filters results based on the keyword "Rust" and sorts them by relevance
82101
"variables": {
83102
"searchGroupFilter": {
84103
"query": "Rust",
@@ -98,9 +117,10 @@ def fetch_groups(endCursor=""):
98117
}
99118
return requests.post(url=URL, headers=headers, json=data)
100119

101-
def get_rush_groups():
120+
def get_rush_groups() -> dict:
102121
"""
103-
Return a dictionary of groups
122+
Returns a dictionary where each key represents the unique ID of a group, and the corresponding value is another dictionary containing details about the group such as name, link, URL name, latitude, and longitude
123+
:rtype: dict
104124
"""
105125
endCursor = None
106126
groups = dict()
@@ -119,15 +139,19 @@ def get_rush_groups():
119139
break
120140
return groups
121141

122-
def get_known_rush_groups(fileName):
142+
def get_known_rush_groups(fileName="rust_meetup_groups.csv") -> dict:
123143
"""
124-
Read url and location of groups. Extract the urlname from the url
125-
Return a dictionary of groups
144+
Returns a dictionary represents all groups from a specified CSV file
145+
:type fileName: Name or Path of the CSV file that contains the URLs and locations of the groups.
126146
"""
127-
groups = dict()
147+
148+
# Reads the CSV file, specifically extracting data from the 'url' and 'location' columns
149+
groups = dict() # main dictionary that stores all information of different groups
128150
df = pd.read_csv(fileName, header=0, usecols=['url', 'location'])
129151

130-
# Format: [source](https://stackoverflow.com/questions/35616434/how-can-i-get-the-base-of-a-url-in-python)
152+
# Extracting the url name of known Rust groups
153+
# Format of extracting the URL:
154+
# [source](https://stackoverflow.com/questions/35616434/how-can-i-get-the-base-of-a-url-in-python)
131155
# https://www.meetup.com/seattle-rust-user-group/
132156
# split_url.scheme "http"
133157
# split_url.netloc "www.meetup.com"
@@ -142,7 +166,14 @@ def get_known_rush_groups(fileName):
142166
return groups
143167

144168
def get_20_events(groups) -> list[Event]:
145-
events = []
169+
"""
170+
Returns a list where each element is an instance of the Event class, representing event data from the Meetup API
171+
:type groups: A dictionary of groups where each entry contains the group's URL name to make an API request
172+
:rtype: dict
173+
"""
174+
events = [] # main list to store data about each fetched event.
175+
176+
# API Configuration:
146177
URL = "https://api.meetup.com/gql"
147178
access_token, refresh_token = ACCESS_TOKEN, REFRESH_TOKEN
148179

@@ -154,8 +185,9 @@ def get_20_events(groups) -> list[Event]:
154185
"Authorization": f"Bearer {access_token}",
155186
"Content-Type": "application/json",
156187
}
188+
189+
# Constructs and sends a GraphQL query for each group to fetch up to 20 upcoming events from the Meetup API using the group's URL name
157190
data = {}
158-
count = 1
159191
for group in groups.values():
160192
urlName = group["urlname"]
161193
data = {
@@ -194,6 +226,7 @@ def get_20_events(groups) -> list[Event]:
194226
response = requests.post(url=URL, headers=headers, json=data)
195227
data = response.json()["data"]
196228

229+
# Constructs Event with attributes such as title, location, date, URL, and organizer details
197230
if data:
198231
searchGroupByUrlname = data["groupByUrlname"]
199232
if searchGroupByUrlname:
@@ -212,8 +245,11 @@ def get_20_events(groups) -> list[Event]:
212245
virtual = True
213246
if venue["venueType"] != "online":
214247
virtual = False
248+
249+
# Convert obtained latitude and longitude of an event to formatted location
215250
address = (GEOLOCATOR.reverse(str(venue["lat"]) +","+ str(venue["lng"]))).raw["address"]
216251
location = format_location(address)
252+
217253
date = node["dateTime"]
218254
url = node["eventUrl"]
219255
organizerName = group.get("name", urlName)
@@ -222,21 +258,29 @@ def get_20_events(groups) -> list[Event]:
222258
events.append(Event(name, location, date, url, virtual, organizerName, organizerUrl))
223259
return events
224260

225-
def format_location(address):
261+
def format_location(address) -> str:
262+
"""
263+
Helper method to format address of events with required components for a location
264+
:rtype: string
265+
"""
226266
if not address:
227267
return "No location"
228268

229269
# Components in the order for location
230-
components = ['road', 'city', 'state', 'postcode', 'country']
270+
components = ['city', 'state', 'country']
231271

232-
# Get available components
272+
# Get available components, otherwise replace missing component with empty string
233273
location = [address.get(component, "") for component in components]
234274

235-
return ', '.join(location) if location else "No location"
275+
return ','.join(location) if location else "No location"
236276

237277
def get_events() -> list[Event]:
238-
events_meetup_groups = get_20_events(get_rush_groups())
239-
events_known_groups = get_20_events(get_known_rush_groups("rust_meetup_groups.csv"))
240-
return events_meetup_groups + events_known_groups
278+
"""
279+
Returns a list of Event objects querying from known, and Meetup API Rust groups
280+
:rtype: list[Event]
281+
"""
282+
# events_meetup_groups = get_20_events(get_rush_groups())
283+
events_known_groups = get_20_events(get_known_rush_groups())
284+
return events_known_groups
241285

242286
# get_events()

tools/events-automation/jwt_auth.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,47 @@
1010
load_dotenv()
1111

1212
def get_PEM_private_key():
13-
# Load the PRIVATE_KEY in PEM-formatted string from .env to bytes
13+
"""
14+
Loads the PRIVATE_KEY in string from .env file.
15+
Returns it in PEM-formatted bytes
16+
"""
1417
pem_bytes = (os.getenv('PRIVATE_KEY', "")).encode()
1518
return pem_bytes
1619

1720
def get_RSA_private_key():
18-
# Deserializes/Loads the private key from PEM-formatted bytes to an RSA private key object, so we could perform cryptographic operations, such as signing data
21+
"""
22+
Deserializes and sign the private key in PEM-formatted in bytes to an RSA private key object using cryptographic operations.
23+
Returns the RSA private key object
24+
"""
1925
private_key = serialization.load_pem_private_key(
2026
get_PEM_private_key(), password=None, backend=default_backend()
2127
)
2228
return private_key
2329

2430
def get_RSA_public_key():
25-
# Get the corresponding RSA public key object from private_key
31+
"""
32+
Returns the corresponding RSA public key object from RSA private_key
33+
"""
2634
public_key = get_RSA_private_key().public_key()
2735
return public_key
2836

2937
def get_PEM_public_key():
30-
#git, to verify digital signatures
38+
"""
39+
Returns the public key in in PEM-formatted in bytes using RSA public key, to verify digital signatures
40+
"""
3141
pem_bytes = (get_RSA_public_key().public_bytes(
3242
encoding=serialization.Encoding.PEM,
3343
format=serialization.PublicFormat.SubjectPublicKeyInfo
3444
)).decode()
3545
return pem_bytes
3646

47+
# This function is essential for authorize step when calling Meetup API
3748
def generate_signed_jwt():
49+
"""
50+
Generates a JWT:
51+
Encodes and signs the payload using RS256 and the private RSA key, forming a base64-url encoded header, payload, and signature.
52+
Then returns it.
53+
"""
3854
AUTHORIZED_MEMBER_ID = os.getenv('AUTHORIZED_MEMBER_ID', "") # the member id that owns the OAuth Client
3955
CLIENT_KEY = os.getenv('CLIENT_KEY', "")
4056
private_key = get_RSA_private_key()
@@ -44,14 +60,17 @@ def generate_signed_jwt():
4460
"aud": "api.meetup.com",
4561
"exp": (datetime.datetime.utcnow() + datetime.timedelta(hours=24)).timestamp()
4662
}
47-
# Generates a JWT: Encodes and signs the payload using RS256 and the private RSA key, forming a base64-url encoded header, payload, and signature. Then return it.
4863
return jwt.encode(
4964
payload=payload,
5065
key=private_key,
5166
algorithm="RS256"
5267
)
5368

54-
def decode_and_validate_jwt(): #get_token_payload/claims
69+
def decode_and_validate_jwt():
70+
"""
71+
Checks/Validates the signed jwt.
72+
Returns a decoded jwt payload/claim
73+
"""
5574
token = generate_signed_jwt()
5675
pem_public_key = get_PEM_public_key()
5776
try:

tools/events-automation/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# collect all the events from the event sources
33
# call event sink with our collected events
44
# print to console / output to file formatted markdown
5+
from generate_events_meetup import get_events as get_meetup_events
56

67
"""
78
Example Markdown format:
@@ -20,10 +21,11 @@
2021
from test_events import get_test_events
2122

2223
def main():
23-
event_list = get_test_events()
24+
event_list = get_meetup_events()
2425
sort_and_filter_events(event_list)
2526
for event in event_list:
2627
print(event.to_markdown_string())
28+
print()
2729

2830

2931
def sort_and_filter_events(event_list):
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
## Guide to Generating Events Using Meetup APIs
2+
3+
### Introduction
4+
This guide provides step-by-step instructions on how to use Meetup APIs to fetch event data related to Rust programming groups. It outlines the installation of necessary packages, setting up environment variables, and executing the script to obtain event data.
5+
6+
### Prerequisites
7+
Before you start, ensure you have the following:
8+
- Python installed on your system (Python 3.8 or later recommended).
9+
- `pip` for managing Python packages.
10+
- Access to terminal or command line.
11+
12+
### Tips for Managing Python Packages
13+
14+
- **Virtual Environment**: Create a virtual environment using `venv` (built into Python 3) or `virtualenv`. Here's how to activate a virtual environment:
15+
```bash
16+
# Creating a virtual environment (for Linux/macOS)
17+
python3 -m venv myenv
18+
# Activating the virtual environment (for Linux/macOS)
19+
source myenv/bin/activate
20+
21+
or
22+
23+
# Creating a virtual environment (for Windows)
24+
python -m venv myenv
25+
# Activating the virtual environment (for Windows)
26+
myenv\Scripts\activate
27+
```
28+
- **Upgrade `pip`**: Ensure your `pip` is up-to-date to avoid installation issues with newer packages:
29+
```bash
30+
pip install --upgrade pip
31+
```
32+
33+
### Installation
34+
1. **Open Terminal or Command Prompt**:
35+
Navigate to the directory `.../tools/events-automation` where `requirements.txt` file is located.
36+
37+
2. **Run the Installation Command**:
38+
Execute the following command to install all the packages listed in `requirements.txt`:
39+
```bash
40+
pip install -r requirements.txt
41+
```
42+
43+
3. **Set Up Environment Variables**:
44+
- Create a `.env` file for project directory.
45+
- Add the following environment variables with your actual values:
46+
```
47+
AUTHORIZED_MEMBER_ID=<Your_Meetup_Member_ID>
48+
CLIENT_KEY=<Your_Meetup_Client_Key>
49+
PRIVATE_KEY=<Your_RSA_Private_Key>
50+
```
51+
These values are used for authentication with the Meetup API and to generate JWT tokens securely.
52+
53+
### Running the Script
54+
To fetch events, run the following command from the project directory `.../tools/events-automation`:
55+
```bash
56+
python main.py
57+
```
58+
This script performs the following operations:
59+
- Authenticates with the Meetup API using JWT.
60+
- Fetches data for known Rust groups from a CSV file and Meetup API.
61+
- Filters and formats the event data into a standardized structure.
62+
- Outputs the details of upcoming events.
63+
64+
### Example Output Format
65+
The script outputs event details in the following format:
66+
```
67+
* 2025-05-08T19:00+02:00 | Virtual (,,Tuvalu) | [rust-noris](TODO: ORGANISER URL HERE)
68+
*[**Rust Nürnberg online**](https://www.meetup.com/rust-noris/events/gmkpctyhchblb)
69+
```
70+
This format includes the date and time of the event, the location (virtual in this case), and links to both the organizer's profile and the event itself.
71+
72+
### Challenges and Considerations
73+
- **Data Accuracy**: The script uses "Rust" as a keyword for searching groups and events. This can sometimes pull in events that merely mention Rust but aren't directly related to Rust programming in the event descriptions.
74+
- **Event Data Completeness**: Not all events have complete venue information, especially for virtual events. The script currently ignores handling missing data, but the ideal is flagging such events for manual review or excluding them from the final output.
75+
- **API Limitations**: The Meetup API has rate limits and other constraints that may affect how frequently you can fetch data.

0 commit comments

Comments
 (0)