geev-unofficial-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geev_unofficial_api-0.1.0/PKG-INFO +407 -0
- geev_unofficial_api-0.1.0/README.md +396 -0
- geev_unofficial_api-0.1.0/geev/__init__.py +62 -0
- geev_unofficial_api-0.1.0/geev/_http.py +236 -0
- geev_unofficial_api-0.1.0/geev/articles.py +285 -0
- geev_unofficial_api-0.1.0/geev/auth.py +141 -0
- geev_unofficial_api-0.1.0/geev/client.py +304 -0
- geev_unofficial_api-0.1.0/geev/conversations.py +114 -0
- geev_unofficial_api-0.1.0/geev/exceptions.py +46 -0
- geev_unofficial_api-0.1.0/geev/models.py +306 -0
- geev_unofficial_api-0.1.0/geev/users.py +152 -0
- geev_unofficial_api-0.1.0/geev_unofficial_api.egg-info/PKG-INFO +407 -0
- geev_unofficial_api-0.1.0/geev_unofficial_api.egg-info/SOURCES.txt +17 -0
- geev_unofficial_api-0.1.0/geev_unofficial_api.egg-info/dependency_links.txt +1 -0
- geev_unofficial_api-0.1.0/geev_unofficial_api.egg-info/requires.txt +4 -0
- geev_unofficial_api-0.1.0/geev_unofficial_api.egg-info/top_level.txt +1 -0
- geev_unofficial_api-0.1.0/pyproject.toml +28 -0
- geev_unofficial_api-0.1.0/setup.cfg +4 -0
- geev_unofficial_api-0.1.0/tests/test_live.py +245 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geev-unofficial-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Synchronous client library for the Geev peer-to-peer donation/sales app (https://www.geev.fr).
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: requests>=2.28
|
|
9
|
+
Provides-Extra: tests
|
|
10
|
+
Requires-Dist: pytest>=7; extra == "tests"
|
|
11
|
+
|
|
12
|
+
# geev_unofficial_api
|
|
13
|
+
|
|
14
|
+
A **synchronous** Python client library for the Geev API (`https://prod.geev.fr`).
|
|
15
|
+
It reproduces exactly what the Geev Android app (v8.6.2) sends on the wire -
|
|
16
|
+
headers, HMAC request signing and multipart bodies - so it works against the
|
|
17
|
+
live service without scraping the website.
|
|
18
|
+
|
|
19
|
+
The library is object-oriented: `GeevClient` is the entry point, and the
|
|
20
|
+
network-accessing entities are `User` and `Article`. **Nothing is fetched at
|
|
21
|
+
object construction** - every method performs its own HTTP request only when
|
|
22
|
+
you call it.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from geev import GeevClient
|
|
26
|
+
|
|
27
|
+
geev = GeevClient()
|
|
28
|
+
geev.login("you@example.com", "s3cret") # returns Session, stored on client
|
|
29
|
+
|
|
30
|
+
# A User handle (no network call yet) ...
|
|
31
|
+
user = geev.get_user("618287b1fafd81627a9ad69d")
|
|
32
|
+
profile = user.profile() # GET /v3/users/{id} (lazy)
|
|
33
|
+
page = user.articles(operation="donations") # GET /v3/users/{id}/items
|
|
34
|
+
|
|
35
|
+
# ... and Articles
|
|
36
|
+
article = geev.get_article(page.items[0]["id"])
|
|
37
|
+
print(article.title, article.is_reservable)
|
|
38
|
+
article.details() # GET /v3/items/{id} (lazy)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Table of contents
|
|
42
|
+
|
|
43
|
+
1. [Install](#1-install)
|
|
44
|
+
2. [Quick examples](#2-quick-examples)
|
|
45
|
+
3. [API reference](#3-api-reference)
|
|
46
|
+
- [GeevClient](#31-geevclient)
|
|
47
|
+
- [User](#32-user)
|
|
48
|
+
- [Article](#33-article)
|
|
49
|
+
- [Auth flow](#34-auth-flow--signup-signin-logout)
|
|
50
|
+
4. [Models](#4-models)
|
|
51
|
+
5. [Errors](#5-errors)
|
|
52
|
+
6. [How it matches the app](#6-how-it-matches-the-app)
|
|
53
|
+
7. [Testing](#7-testing)
|
|
54
|
+
8. [Project layout](#8-project-layout)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 1. Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv sync
|
|
62
|
+
pip install -e .
|
|
63
|
+
# optional, for tests
|
|
64
|
+
pip install -e ".[tests]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Requires Python ≥ 3.11 and `requests`.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 2. Quick examples
|
|
72
|
+
|
|
73
|
+
### Sign in and explore a user's donations
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from geev import GeevClient
|
|
77
|
+
|
|
78
|
+
geev = GeevClient() # prod API by default
|
|
79
|
+
session = geev.login("jane@example.com", "s3cret")
|
|
80
|
+
|
|
81
|
+
user = geev.get_user("618287b1fafd81627a9ad69d")
|
|
82
|
+
page = user.articles(operation="donations", limit=10)
|
|
83
|
+
print(len(page.items), "articles; next cursor:", page.next_after)
|
|
84
|
+
|
|
85
|
+
for raw in page.items:
|
|
86
|
+
print(raw["id"], raw["title"], raw.get("status"))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Search offers
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
geev = GeevClient()
|
|
93
|
+
geev.login(email, password)
|
|
94
|
+
|
|
95
|
+
results = geev.search_articles(text="chaise", limit=20) # placement defaults to top_categories
|
|
96
|
+
for article in results:
|
|
97
|
+
print(article.id, article.title, article.city, article.is_reservable)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Reserve / order an article
|
|
101
|
+
|
|
102
|
+
While the API has a reservation endpoint, ordering another user's item is a
|
|
103
|
+
**destructive side effect** on the platform - use with care and only with
|
|
104
|
+
accounts you control:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
session = geev.login(email, password)
|
|
108
|
+
article = geev.get_article(ARTICLE_ID)
|
|
109
|
+
reservation = article.reserve() # recipient = logged-in user
|
|
110
|
+
print(reservation.reservationId)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Lazy user methods
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
user = geev.get_user("618287b1fafd81627a9ad69d")
|
|
117
|
+
user.profile() # first call does the network round-trip
|
|
118
|
+
user.articles() # ...
|
|
119
|
+
user.carbon_summary() # ... on demand, not at construction
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 3. API reference
|
|
125
|
+
|
|
126
|
+
### 3.1 `GeevClient`
|
|
127
|
+
|
|
128
|
+
`geev.GeevClient(base_url=None, language="fr", token=None, session=None)`
|
|
129
|
+
|
|
130
|
+
| Arg | Default | Meaning |
|
|
131
|
+
|-----|---------|---------|
|
|
132
|
+
| `base_url` | `https://prod.geev.fr/v3` | also `https://dev.geev.fr/v3`, `https://stage.geev.fr/v3` |
|
|
133
|
+
| `language` | `"fr"` | value of the `language` header on every call |
|
|
134
|
+
| `token` | `None` | skip login if you already have an `appToken` |
|
|
135
|
+
| `session` | `None` | a pre-built `Session` (userId + token) |
|
|
136
|
+
|
|
137
|
+
The client stores the current token on `.token` and the full session on
|
|
138
|
+
`.session`, and passes the token to every authenticated request.
|
|
139
|
+
|
|
140
|
+
**Auth**
|
|
141
|
+
|
|
142
|
+
| Method | Endpoint | Notes |
|
|
143
|
+
|--------|----------|-------|
|
|
144
|
+
| `check_email(email) -> bool` | `POST /auth/email/check` | True if available |
|
|
145
|
+
| `signup(first_name, last_name, email, password, marketing_consent=False, opted_out=False, pixel_consent=False, picture_path=None) -> Registration` | `POST /accounts/local` (multipart) | returns `accountId`/`userId`; account not yet active |
|
|
146
|
+
| `resend_validation(account_id)` | `POST /accounts/{accountId}/resend-validation` | - |
|
|
147
|
+
| `validate_account(account_id, code) -> Session` | `POST /accounts/{accountId}/validate` | activates account, stores token |
|
|
148
|
+
| `login(email, password) -> Session` | `POST /auth/local/login` | stores token |
|
|
149
|
+
| `logout()` | `POST /auth/logout` | **destructive**: invalidates the token |
|
|
150
|
+
|
|
151
|
+
`signup` returns a `Registration`; you then validate with the 6-digit code
|
|
152
|
+
emailed by Geev:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
reg = geev.signup(first_name="Jane", last_name="Doe",
|
|
156
|
+
email="jane@example.com", password="S3cret!")
|
|
157
|
+
geev.validate_account(reg.accountId, "123456") # code from the email
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Articles**
|
|
161
|
+
|
|
162
|
+
| Method | Endpoint | Notes |
|
|
163
|
+
|--------|----------|-------|
|
|
164
|
+
| `search_articles(text=None, article_type=None, states=None, categories=None, distance=None, latitude=None, longitude=None, placement="top_categories", mode="standard", limit=20, skip=1) -> List[Article]` | `POST /search/items` | `skip` is 1-based (0 is rejected); `placement` is one of the server's accepted values, see below |
|
|
165
|
+
| `get_article(article_id) -> Article` | `GET /items/{articleId}` | wraps the payload |
|
|
166
|
+
| `reserve_article(article_id, recipient_user_id=None) -> Reservation` | `POST /reservations` | defaults the recipient to the logged-in user |
|
|
167
|
+
|
|
168
|
+
`placement` values accepted by the server: `home_listing`, `top_categories`,
|
|
169
|
+
`home_exclusivities`, `home_near_you`, `home_sales`,
|
|
170
|
+
`my_formula_contact_advantages`, `not_found`, `explorer`,
|
|
171
|
+
`favorites_carousel`. `top_categories` supports keyword `text` filters.
|
|
172
|
+
|
|
173
|
+
**Messaging / contact the vendor**
|
|
174
|
+
|
|
175
|
+
| Method | Endpoint | Notes |
|
|
176
|
+
|--------|----------|-------|
|
|
177
|
+
| `get_conversation(conversation_id) -> Conversation` | `GET /conversations/{conversationId}` | fetch thread + history |
|
|
178
|
+
| `contact_article(article_id, message, dry_run=False, confirm=False) -> Conversation` | `POST /items/{articleId}/contact` | starts/reuses the chat with the author |
|
|
179
|
+
| `request_adoption(article_id, message, dry_run=False) -> dict` | `POST /adoptions` | `{itemIds, message}` - expresses intent, does **not** reserve |
|
|
180
|
+
| `list_conversations(item_id=None, with_archived=False) -> list` | `GET /self/conversations` | one article summary per thread |
|
|
181
|
+
|
|
182
|
+
**Inbox, reserved deals, delivery**
|
|
183
|
+
|
|
184
|
+
| Method | Endpoint | Notes |
|
|
185
|
+
|--------|----------|-------|
|
|
186
|
+
| `get_inbox(with_archived=False) -> List[ConversationSummary]` | `GET /self/conversations` | inbox: one summary per thread, with latest message + unread count |
|
|
187
|
+
| `get_reserved_collections() -> List[ConversationSummary]` | ... | inbox entries where a deal is `reserved` (vendor accepted) |
|
|
188
|
+
| `confirm_adoption(reservation_id, *, communication_grade, punctuality_grade, feedback=None) -> AdoptionConfirmed` | `PATCH /reservations/{id}/confirm-adoption` | adopter confirms the donation was delivered; closes the deal |
|
|
189
|
+
| `confirm_order(article_id, *, recipient_user_id=None, firstname=None, lastname=None) -> OrderConfirmed` | `POST /reservations` | buyer confirms a sale order |
|
|
190
|
+
|
|
191
|
+
**Users**
|
|
192
|
+
|
|
193
|
+
| Method | Endpoint | Notes |
|
|
194
|
+
|--------|----------|-------|
|
|
195
|
+
| `get_user(user_id) -> User` | – | no network call |
|
|
196
|
+
| `get_me() -> User` | – | a `User` handle for the logged-in session (`session.userId`); no network call |
|
|
197
|
+
|
|
198
|
+
### 3.2 `User`
|
|
199
|
+
|
|
200
|
+
`geev.users.User` is created via `client.get_user(user_id)` and fetches on
|
|
201
|
+
demand. All listing/profile calls require the client to be logged in.
|
|
202
|
+
|
|
203
|
+
| Method | Endpoint | Return |
|
|
204
|
+
|--------|----------|--------|
|
|
205
|
+
| `profile()` | `GET /v3/users/{userId}` | raw dict (`firstName`, `lastName`, `firstIntention`, `_links`, ...) |
|
|
206
|
+
| `first_name`, `last_name` (properties) | – | called `profile()` lazily |
|
|
207
|
+
| `articles(operation="donations", status=None, after=None, limit=50) -> Page` | `GET /v3/users/{userId}/items` | `Page{items, next_after, raw}` |
|
|
208
|
+
| `iter_articles(operation="donations", status=None, page_size=50) -> Iterator[dict]` | same, cursor-following | yields every item across pages |
|
|
209
|
+
| `reviews(type=None, after=None, limit=20) -> List[Review]` | `GET /v3/users/{userId}/reviews` | - |
|
|
210
|
+
| `carbon_summary(temporality=None, light=False) -> CarbonSummary` | `GET /v3/users/{id}/carbonSummary` | `temporality` ∈ `ever`, `thisYear`, `thisMonth` |
|
|
211
|
+
|
|
212
|
+
`operation` is **required** by the server: `donations` or `requests`. For
|
|
213
|
+
`donations`, pass `status=["AVAILABLE"]` to see only what can be ordered
|
|
214
|
+
today; the app's default is `["AVAILABLE","RESERVED","GIVEN","ACQUIRED"]`.
|
|
215
|
+
The response exposes a cursor in `Page.next_after` (an article id) for the
|
|
216
|
+
next page.
|
|
217
|
+
|
|
218
|
+
### 3.3 `Article`
|
|
219
|
+
|
|
220
|
+
`geev.articles.Article` wraps a listing/search payload. Convenience read-only
|
|
221
|
+
properties (`id`, `title`, `description`, `type`, `state`, `status`,
|
|
222
|
+
`category`, `universe`, `picture`, `pictures`, `city`, `author_id`,
|
|
223
|
+
`author_name`, `carbon_value`, `savings`, `price`, `stock`, `validated`,
|
|
224
|
+
`is_reservable`) never hit the network - they read the payload that created
|
|
225
|
+
the object.
|
|
226
|
+
|
|
227
|
+
| Method | Endpoint | Return |
|
|
228
|
+
|--------|----------|--------|
|
|
229
|
+
| `details()` | `GET /v3/items/{articleId}` | raw dict with `description`, `status`, `creditCost`, `donator`, `pictures` |
|
|
230
|
+
| `reserve(recipient_user_id=None) -> Reservation` | `POST /v3/reservations` | **destructive**; defaults to logged-in user |
|
|
231
|
+
| `related() -> List[Article]` | `GET /v3/items/{id}/related` | similar articles |
|
|
232
|
+
| `contact(message, dry_run=False, confirm=False) -> Conversation` | `POST /v3/items/{id}/contact` | message the vendor; thread is fetched |
|
|
233
|
+
| `request_adoption(message, dry_run=False) -> dict` | `POST /v3/adoptions` | `{itemIds, message}`; intent, no reserve |
|
|
234
|
+
|
|
235
|
+
Example - start a conversation with the vendor of an article:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
article = geev.get_article(ARTICLE_ID)
|
|
239
|
+
conversation = article.contact("Bonjour, c'est encore disponible ?")
|
|
240
|
+
print(conversation.status) # e.g. CONTACTED
|
|
241
|
+
send = conversation.send_message("Parfait, merci !")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If the account has several conversations without a verified phone number, the
|
|
245
|
+
server answers `428` and the payload advertises a `confirmContact` link -
|
|
246
|
+
retry with `**contact(..., confirm=True)**`.
|
|
247
|
+
|
|
248
|
+
### 3.4 `Conversation`
|
|
249
|
+
|
|
250
|
+
`geev.conversations.Conversation` wraps a messaging thread. Created via
|
|
251
|
+
`client.get_conversation(id)`, `article.contact(...)`, or implicitly by
|
|
252
|
+
`client.contact_article(...)`; details are fetched once (populating `.raw`,
|
|
253
|
+
`.item_id`, `.status` and `.messages`).
|
|
254
|
+
|
|
255
|
+
| Field / method | Meaning |
|
|
256
|
+
|----------------|---------|
|
|
257
|
+
| `conversation_id` | thread id |
|
|
258
|
+
| `item_id`, `status`, `messages` | fetched fields (after `fetch()`) |
|
|
259
|
+
| `reservation_id`, `reservation` | deal attached to the thread (after `fetch()`) |
|
|
260
|
+
| `fetch() -> Conversation` | `GET /v3/conversations/{id}` |
|
|
261
|
+
| `send_message(text) -> Message` | `POST /v3/conversations/{id}/message` |
|
|
262
|
+
| `list_open(client, item_id=None, with_archived=False) -> list` | `GET /v3/self/conversations` |
|
|
263
|
+
|
|
264
|
+
Example - complete a donation deal:
|
|
265
|
+
|
|
266
|
+
```python
|
|
267
|
+
reserved = geev.get_reserved_collections()
|
|
268
|
+
deal = next(s for s in reserved if s.given and not s.acquired)
|
|
269
|
+
conversation = geev.get_conversation(deal.conversation_id)
|
|
270
|
+
geev.confirm_adoption(conversation.reservation_id,
|
|
271
|
+
communication_grade=5.0, punctuality_grade=5.0)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 3.5 Auth flow - signup, signin, logout
|
|
275
|
+
|
|
276
|
+
The sign-up flow mirrors the app:
|
|
277
|
+
|
|
278
|
+
1. `check_email(email)` - optional pre-check.
|
|
279
|
+
2. `signup(...)` - multipart `POST /accounts/local`, returns `Registration`.
|
|
280
|
+
3. `validate_account(account_id, code)` - `POST /accounts/{accountId}/validate`;
|
|
281
|
+
the response carries the `appToken` = `X-Geev-Token` used afterwards.
|
|
282
|
+
4. `login(email, password)` - `POST /auth/local/login`, same token mechanism.
|
|
283
|
+
5. `logout()` - `POST /auth/logout`; invalidates the current token
|
|
284
|
+
(subsequent requests will 401).
|
|
285
|
+
|
|
286
|
+
There is **no persistence** in the library: tokens live only in memory on the
|
|
287
|
+
client object. To reuse a session across runs, capture `session.appToken` and
|
|
288
|
+
`session.userId` yourself and build a new client with
|
|
289
|
+
`GeevClient(token=..., session=...)`.
|
|
290
|
+
|
|
291
|
+
> There is **no user-lookup-by-name** endpoint in the Geev API. Users are
|
|
292
|
+
> identified solely by their `userId` (the last path segment of a profile URL
|
|
293
|
+
> like `https://www.geev.fr/profile/<id>`).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## 4. Models
|
|
298
|
+
|
|
299
|
+
| Class | Fields |
|
|
300
|
+
|-------|--------|
|
|
301
|
+
| `Session` | `appToken`, `userId`, `sso`, `userType` |
|
|
302
|
+
| `Registration` | `accountId`, `userId` |
|
|
303
|
+
| `Reservation` | `reservationId`, `itemId`, `raw` |
|
|
304
|
+
| `Page` | `items`, `next_after`, `raw` |
|
|
305
|
+
| `Review` | `id`, `grade`, `message`, `raw` |
|
|
306
|
+
| `CarbonSummary` | `year`, `month`, `carbonValue`, `donations`, `adoptions`, `equivalences`, `raw` |
|
|
307
|
+
| `Location` | `label`, `city`, `postalCode`, `latitude`, `longitude`, `radius`, `obfuscated` |
|
|
308
|
+
| `Message` | `id`, `author_id`, `timestamp`, `text`, `read_by_receiver`, `raw` |
|
|
309
|
+
| `Conversation` | handle class; see §3.4 |
|
|
310
|
+
| `ConversationSummary` | inbox entry: `id`, `title`, `status`, `reserved`/`given`/`acquired`/`closed`, `conversation_id`, `latest_message`, `unseen_count`, `raw` |
|
|
311
|
+
| `OrderConfirmed` | `reservation_id`, `conversation_id`, `raw` |
|
|
312
|
+
| `AdoptionConfirmed` | `big_savings`, `carbon_value`, `savings`, `raw` |
|
|
313
|
+
|
|
314
|
+
Every model also carries the raw server payload in `.raw` so you can access
|
|
315
|
+
fields the library does not wrap yet.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 5. Errors
|
|
320
|
+
|
|
321
|
+
All exceptions derive from `geev.exceptions.GeevError`.
|
|
322
|
+
|
|
323
|
+
| Exception | Raised when |
|
|
324
|
+
|-----------|-------------|
|
|
325
|
+
| `BadRequest` | HTTP 4xx, or a malformed/unexpected body |
|
|
326
|
+
| `ServerError` | HTTP 5xx |
|
|
327
|
+
| `AuthenticationError` | HTTP 401/403, incl. wrong validation code |
|
|
328
|
+
| `ValidationError` | client-side argument validation |
|
|
329
|
+
|
|
330
|
+
`BadRequest` and its subclasses expose `.status_code`, `.payload`, `.method`
|
|
331
|
+
and `.url`.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from geev import GeevClient, AuthenticationError
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
geev.login("jane@example.com", "wrong-password")
|
|
338
|
+
except AuthenticationError as e:
|
|
339
|
+
print(e) # includes HTTP status and payload
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## 6. How it matches the app
|
|
345
|
+
|
|
346
|
+
The library reproduces the exact wire behaviour of Geev 8.6.2:
|
|
347
|
+
|
|
348
|
+
- **Global headers** on every request: `User-Agent`,
|
|
349
|
+
`x-geev-device-model`, `geev-app-version`, `geev-device`, `timezone`,
|
|
350
|
+
plus per-call `language`, `X-Geev-Token` (when logged in), `Content-type`
|
|
351
|
+
and `Accept`.
|
|
352
|
+
- **Request signing** (`x-geev-timestamp` + `x-geev-request-signature`):
|
|
353
|
+
HMAC-SHA256 over `body_bytes || timestamp_ms` with the key extracted from
|
|
354
|
+
the app's `SignatureInterceptor`. Only present when the request has a body.
|
|
355
|
+
In this library the body is serialized *before* signing, so the signed
|
|
356
|
+
bytes are exactly the bytes on the wire.
|
|
357
|
+
- **Multipart** sign-up body is built manually (OkHttp byte-for-byte
|
|
358
|
+
compatible) so signing stays exact.
|
|
359
|
+
|
|
360
|
+
Reverse-engineered from the decompiled APK; the endpoint reference doc is
|
|
361
|
+
`[RAW_API_DOC.md](docs/RAW_API_DOC.md)`.
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 7. Testing
|
|
366
|
+
|
|
367
|
+
The test suite runs against the **live** production API (`prod.geev.fr`). It
|
|
368
|
+
is marked `live`; destructive operations (reserve, logout) are **not**
|
|
369
|
+
executed automatically.
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
pytest tests/test_live.py -m live -v
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Defaults for the provided test account are embedded in `tests/conftest.py`;
|
|
376
|
+
override with environment variables:
|
|
377
|
+
|
|
378
|
+
| Variable | Default |
|
|
379
|
+
|----------|---------|
|
|
380
|
+
| `GEEV_TEST_TOKEN` | the provided `appToken` |
|
|
381
|
+
| `GEEV_TEST_USER` | `6a11e587ef4a89cd2c8ad9ac` |
|
|
382
|
+
| `GEEV_TARGET_USER` | `618287b1fafd81627a9ad69d` |
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## 8. Project layout
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
./
|
|
390
|
+
├── docs
|
|
391
|
+
│ ├── API.md # doc for the endpoints used in this project
|
|
392
|
+
│ └── RAW_API_DOC.md # doc produced by a LLM while reversing the app
|
|
393
|
+
├── pyproject.toml
|
|
394
|
+
├── README.md # this document
|
|
395
|
+
├── geev/
|
|
396
|
+
│ ├── __init__.py # public exports
|
|
397
|
+
│ ├── _http.py # headers, signing, multipart, transport
|
|
398
|
+
│ ├── exceptions.py # error types
|
|
399
|
+
│ ├── models.py # value objects (Session, Page, ...)
|
|
400
|
+
│ ├── auth.py # signup / signin / logout / validate
|
|
401
|
+
│ ├── users.py # User class + user operations
|
|
402
|
+
│ ├── articles.py # Article class + search / reserve
|
|
403
|
+
│ └── client.py # GeevClient facade
|
|
404
|
+
└── tests/
|
|
405
|
+
├── conftest.py # fixtures (live API credentials)
|
|
406
|
+
└── test_live.py # live API tests
|
|
407
|
+
```
|