Skip to content

Commit 9b6097c

Browse files
authored
Merge pull request #1 from amaanjaved1/feature/Major-code-changes
Feature/major code changes
2 parents e5c9636 + d4cc620 commit 9b6097c

19 files changed

Lines changed: 3035 additions & 155 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,9 @@ htmlcov/
2222

2323
# mypy
2424
.mypy_cache/
25+
26+
# Python bytecode / cache
27+
__pycache__/
28+
*.py[cod]
29+
*$py.class
30+
*.pyc

README.md

Lines changed: 240 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,262 @@ pip install 'ratemyprofessors-client[sentiment]'
2121

2222
## Quickstart
2323

24+
**Professor by ID** (data from professor page HTML):
25+
2426
```python
2527
from rmp_client import RMPClient
2628

27-
SCHOOL_ID = 1466 # example: Queen's University ID on RMP
29+
with RMPClient() as client:
30+
professor = client.get_professor("2823076") # legacy ID from URL
31+
print(professor.name, professor.overall_rating, professor.num_ratings, professor.school.name)
32+
```
2833

34+
**School by ID** (data from school page HTML):
35+
36+
```python
2937
with RMPClient() as client:
30-
for prof in client.iter_professors_for_school(SCHOOL_ID, page_size=20):
31-
print(prof.name, prof.overall_rating, prof.num_ratings)
38+
school = client.get_school("1466")
39+
print(school.name, school.location, school.overall_quality, school.num_ratings)
3240
```
3341

34-
Fetch details and iterate ratings incrementally:
42+
**Search professors or schools** (data from search page HTML):
43+
44+
```python
45+
with RMPClient() as client:
46+
profs = client.search_professors("test")
47+
print(profs.total, profs.has_next_page)
48+
for p in profs.professors[:5]:
49+
print(p.name, p.school.name if p.school else "")
50+
51+
schools = client.search_schools("queens")
52+
for s in schools.schools:
53+
print(s.name, s.location)
54+
```
55+
56+
**Compare two schools** (data from compare page HTML):
57+
58+
```python
59+
with RMPClient() as client:
60+
result = client.get_compare_schools("1466", "1491")
61+
print(result.school_1.name, result.school_2.name)
62+
```
63+
64+
**Iterate professor ratings** (first page from HTML, further pages via GraphQL):
3565

3666
```python
3767
from datetime import date
3868
from rmp_client import RMPClient
3969

4070
with RMPClient() as client:
41-
professor = client.get_professor("PROFESSOR_ID")
42-
43-
for rating in client.iter_professor_ratings(professor.id, since=date(2024, 1, 1)):
71+
for rating in client.iter_professor_ratings("2823076", since=date(2024, 1, 1)):
4472
print(rating.date, rating.quality, rating.comment)
4573
```
4674

75+
**Verify the client** (run the script to hit the live site and print sample data):
76+
77+
```bash
78+
pip install -e .
79+
python scripts/verify_client.py # up to 3 pages of ratings per section (default)
80+
python scripts/verify_client.py --max-pages 10 --page-size 20 # scrape more pages
81+
```
82+
83+
**Scrape all ratings** for a professor or school: the client fetches the first page from HTML and subsequent pages via the site’s GraphQL API. Use the iterators to get every rating:
84+
85+
```python
86+
with RMPClient() as client:
87+
for rating in client.iter_professor_ratings("2823076"):
88+
print(rating.date, rating.comment)
89+
for rating in client.iter_school_ratings("1466"):
90+
print(rating.date, rating.comment)
91+
```
92+
93+
## How it works
94+
95+
### Package architecture
96+
97+
```mermaid
98+
flowchart TB
99+
subgraph Your code
100+
User["Your script / app"]
101+
end
102+
103+
subgraph rmp_client [rmp_client package]
104+
Client["RMPClient\n(client.py)"]
105+
Config["RMPClientConfig\n(config.py)"]
106+
Models["Models\n(School, Professor, Rating)\n(models.py)"]
107+
Errors["RMPError hierarchy\n(errors.py)"]
108+
end
109+
110+
subgraph HTTP layer
111+
HttpCtx["HttpClientContext\n(http.py)"]
112+
Http["HttpClient\n(retries, headers)"]
113+
Bucket["TokenBucket\n(rate_limit.py)"]
114+
end
115+
116+
subgraph External
117+
RMP["RMP pages\n(ratemyprofessors.com)"]
118+
end
119+
120+
User --> Client
121+
Client --> Config
122+
Client --> HttpCtx
123+
HttpCtx --> Http
124+
Http --> Bucket
125+
Http --> RMP
126+
Client --> Models
127+
Client --> Errors
128+
```
129+
130+
### Request flow
131+
132+
Professor, school, compare-schools, and search endpoints **fetch the relevant RMP page HTML** (GET), extract `window.__RELAY_STORE__` from the response, and parse it into `Professor`, `School`, `Rating`, or search result lists.
133+
134+
**Ratings pagination (Relay):** The first page of professor or school ratings comes from the same HTML (Relay store). The store’s connection includes:
135+
136+
- **`pageInfo.endCursor`** — opaque cursor for “start after this item”
137+
- **`pageInfo.hasNextPage`** — whether more ratings exist
138+
139+
The client then requests the next page by POSTing to `/graphql` with the same query and variables:
140+
141+
- `id` — Relay node id (base64 of `Teacher-{legacyId}` or `School-{legacyId}`)
142+
- `first` — page size (e.g. 20)
143+
- `after``pageInfo.endCursor` from the previous response
144+
145+
Loop until `hasNextPage` is false. The cursor is typically base64 for an internal offset (e.g. `YXJyYXljb25uZWN0aW9uOjQ=` decodes to `arrayconnection:4`, meaning “after item 4”). RMP does not rotate or expire these cursors, so you can paginate with plain HTTP requests without a browser. This client sends the **full GraphQL query** in each request; if the site ever required persisted queries (e.g. `doc_id` only), you’d capture the real request from the browser and reuse that format.
146+
147+
```mermaid
148+
sequenceDiagram
149+
participant User
150+
participant RMPClient
151+
participant HttpClient
152+
participant TokenBucket
153+
participant httpx
154+
participant RMP
155+
156+
User->>RMPClient: e.g. get_professor(id), get_school(id), search_professors(q), get_compare_schools(id1, id2)
157+
RMPClient->>HttpClient: get_html(url)
158+
HttpClient->>TokenBucket: consume()
159+
TokenBucket-->>HttpClient: (blocks until token available)
160+
HttpClient->>httpx: GET page URL
161+
httpx->>RMP: HTTPS request
162+
RMP-->>httpx: HTML (with __RELAY_STORE__)
163+
httpx-->>HttpClient: response
164+
HttpClient-->>RMPClient: HTML text
165+
RMPClient->>RMPClient: Extract and parse __RELAY_STORE__, resolve refs
166+
RMPClient->>RMPClient: Map to Professor / School / Rating / SearchResult
167+
RMPClient-->>User: Professor, School, list, or CompareSchoolsResult
168+
```
169+
170+
### Data models
171+
172+
```mermaid
173+
erDiagram
174+
School ||--o{ Professor : "has"
175+
Professor ||--o{ Rating : "has"
176+
177+
School {
178+
string id
179+
string name
180+
string location
181+
float overall_quality
182+
int num_ratings
183+
}
184+
185+
Professor {
186+
string id
187+
string name
188+
string department
189+
float overall_rating
190+
int num_ratings
191+
School school
192+
}
193+
194+
Rating {
195+
date date
196+
string comment
197+
float quality
198+
float difficulty
199+
string course_raw
200+
}
201+
202+
ProfessorSearchResult {
203+
Professor[] professors
204+
int total
205+
bool has_next_page
206+
string next_cursor
207+
}
208+
209+
SchoolSearchResult {
210+
School[] schools
211+
int total
212+
bool has_next_page
213+
string next_cursor
214+
}
215+
216+
CompareSchoolsResult {
217+
School school_1
218+
School school_2
219+
}
220+
221+
ProfessorRatingsPage {
222+
Professor professor
223+
Rating[] ratings
224+
bool has_next_page
225+
string next_cursor
226+
}
227+
```
228+
229+
### Extras and ingestion pipeline
230+
231+
```mermaid
232+
flowchart LR
233+
subgraph RMPClient
234+
iter_professors["iter_professors_for_school"]
235+
iter_ratings["iter_professor_ratings"]
236+
end
237+
238+
subgraph extras [rmp_client.extras]
239+
dedupe["dedupe\n(normalize_comment,\n is_valid_comment)"]
240+
sentiment["sentiment\n(analyze_sentiment)"]
241+
course_codes["course_codes\n(build_course_mapping)"]
242+
end
243+
244+
subgraph Your pipeline [Your pipeline e.g. ingest_supabase]
245+
filter["Filter comments"]
246+
store["Supabase / DB"]
247+
end
248+
249+
iter_professors --> iter_ratings
250+
iter_ratings --> filter
251+
filter --> dedupe
252+
dedupe --> sentiment
253+
iter_ratings --> course_codes
254+
sentiment --> store
255+
course_codes --> store
256+
```
257+
258+
### CI/CD (publish to PyPI)
259+
260+
```mermaid
261+
flowchart LR
262+
subgraph On any push
263+
T[Run tests\npytest]
264+
end
265+
266+
subgraph On main push
267+
B[Build wheel + sdist]
268+
TestPyPI[Publish to TestPyPI]
269+
end
270+
271+
subgraph On release published
272+
PyPI[Publish to PyPI]
273+
end
274+
275+
T --> B
276+
B --> TestPyPI
277+
B --> PyPI
278+
```
279+
47280
## Extras
48281

49282
Optional helpers live under `rmp_client.extras`:

0 commit comments

Comments
 (0)