This commit is contained in:
parent
246cdf51fa
commit
5a0a066105
5
.gitignore
vendored
5
.gitignore
vendored
@ -2,3 +2,8 @@
|
|||||||
.history
|
.history
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
examples/crawler/devrant.sqlite-shm
|
||||||
|
examples/crawler/devrant.sqlite-wal
|
||||||
|
examples/crawler/devrant.sqlite
|
||||||
|
examples/crawler/.venv
|
||||||
|
examples/crawler/__pycache__
|
||||||
|
344
README.md
344
README.md
@ -1,66 +1,38 @@
|
|||||||
# devRanta
|
# devRanta
|
||||||
|
|
||||||
devRanta is the best async devRant client written in Python. Authentication is only needed for half of the functionality; thus, the username and password are optional parameters when constructing the main class of this package (Api). You can find the latest packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages).
|
devRanta is the best async devRant client written in Python. Authentication is only needed for half of the functionality; thus, the username and password are optional parameters when constructing the main class of this package (Api). You can find the latest packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages).
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
```
|
```
|
||||||
make run
|
make run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
Tests are only made for methods not requireing authentication.
|
Tests are only made for methods not requireing authentication.
|
||||||
I do not see value in mocking requests.
|
I do not see value in mocking requests.
|
||||||
```
|
```
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
Implementation:
|
Implementation:
|
||||||
```
|
```
|
||||||
from devranta.api import Api
|
from devranta.api import Api
|
||||||
|
|
||||||
api = Api(username="optional!", password="optional!")
|
api = Api(username="optional!", password="optional!")
|
||||||
|
|
||||||
async def list_rants():
|
async def list_rants():
|
||||||
async for rant in api.get_rants():
|
async for rant in api.get_rants():
|
||||||
print(rant["user_username"], ":", rant["text"])
|
print(rant["user_username"], ":", rant["text"])
|
||||||
```
|
```
|
||||||
See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use.
|
See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use.
|
||||||
|
|
||||||
|
|
||||||
# devRant API Documentation
|
# devRant API Documentation
|
||||||
|
|
||||||
For people wanting to build their own client.
|
For people wanting to build their own client.
|
||||||
|
|
||||||
TODO: document responses.
|
TODO: document responses.
|
||||||
|
|
||||||
## Base URL
|
## Base URL
|
||||||
`https://devrant.com/api`
|
`https://devrant.com/api`
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
- Uses `dr_token` cookie with `token_id`, `token_key`, and `user_id`.
|
- Uses `dr_token` cookie with `token_id`, `token_key`, and `user_id`.
|
||||||
- Required for endpoints needing user authentication.
|
- Required for endpoints needing user authentication.
|
||||||
- `guid`, `plat`, `sid`, `seid` included in requests for session tracking.
|
- `guid`, `plat`, `sid`, `seid` included in requests for session tracking.
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
### User Management
|
### User Management
|
||||||
1. **Register User**
|
1. **Registering user**
|
||||||
- **URL**: `/api/users`
|
- Ommitted, you know why.
|
||||||
- **Method**: POST
|
|
||||||
- **Parameters**:
|
|
||||||
- `app`: 3 (constant)
|
|
||||||
- `type`: 1 (constant)
|
|
||||||
- `email`: User email
|
|
||||||
- `username`: User username
|
|
||||||
- `password`: User password
|
|
||||||
- `guid`: Unique identifier (from `getMyGuid`)
|
|
||||||
- `plat`: 3 (constant)
|
|
||||||
- `sid`: Session start time (from `getSessionStartTime`)
|
|
||||||
- `seid`: Session event ID (from `getSessionEventId`)
|
|
||||||
- **Response**: JSON with `success`, `auth_token`, or `error` and `error_field`
|
|
||||||
- **Description**: Creates a new user account.
|
|
||||||
|
|
||||||
2. **Login User**
|
2. **Login User**
|
||||||
- **URL**: `/api/users/auth-token`
|
- **URL**: `/api/users/auth-token`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -73,8 +45,26 @@ TODO: document responses.
|
|||||||
- `sid`: Session start time
|
- `sid`: Session start time
|
||||||
- `seid`: Session event ID
|
- `seid`: Session event ID
|
||||||
- **Response**: JSON with `success`, `auth_token`, or `error`
|
- **Response**: JSON with `success`, `auth_token`, or `error`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"auth_token": {
|
||||||
|
"id": 18966518,
|
||||||
|
"key": "z6uXRZrQ_Ekx59wfYEjpbnS!fDeVznVqmmKujhT8",
|
||||||
|
"expire_time": 1756765587,
|
||||||
|
"user_id": 18959632
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Invalid login credentials entered. Please try again."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Authenticates user and returns auth token.
|
- **Description**: Authenticates user and returns auth token.
|
||||||
|
|
||||||
3. **Edit Profile**
|
3. **Edit Profile**
|
||||||
- **URL**: `/api/users/me/edit-profile`
|
- **URL**: `/api/users/me/edit-profile`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -90,8 +80,13 @@ TODO: document responses.
|
|||||||
- `profile_website`: User website
|
- `profile_website`: User website
|
||||||
- `profile_github`: GitHub username
|
- `profile_github`: GitHub username
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Updates user profile information.
|
- **Description**: Updates user profile information.
|
||||||
|
|
||||||
4. **Forgot Password**
|
4. **Forgot Password**
|
||||||
- **URL**: `/api/users/forgot-password`
|
- **URL**: `/api/users/forgot-password`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -100,8 +95,13 @@ TODO: document responses.
|
|||||||
- `username`: User username
|
- `username`: User username
|
||||||
- `guid`, `plat`, `sid`, `seid`
|
- `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Initiates password reset process.
|
- **Description**: Initiates password reset process.
|
||||||
|
|
||||||
5. **Resend Confirmation Email**
|
5. **Resend Confirmation Email**
|
||||||
- **URL**: `/api/users/me/resend-confirm`
|
- **URL**: `/api/users/me/resend-confirm`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -109,8 +109,13 @@ TODO: document responses.
|
|||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Resends account confirmation email.
|
- **Description**: Resends account confirmation email.
|
||||||
|
|
||||||
6. **Delete Account**
|
6. **Delete Account**
|
||||||
- **URL**: `/api/users/me`
|
- **URL**: `/api/users/me`
|
||||||
- **Method**: DELETE
|
- **Method**: DELETE
|
||||||
@ -119,7 +124,6 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
- **Description**: Deletes user account.
|
- **Description**: Deletes user account.
|
||||||
|
|
||||||
7. **Mark News as Read**
|
7. **Mark News as Read**
|
||||||
- **URL**: `/api/users/me/mark-news-read`
|
- **URL**: `/api/users/me/mark-news-read`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -128,8 +132,13 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `news_id`: News item ID
|
- `news_id`: News item ID
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Marks a news item as read for logged-in users.
|
- **Description**: Marks a news item as read for logged-in users.
|
||||||
|
|
||||||
### Rants
|
### Rants
|
||||||
1. **Get Rant**
|
1. **Get Rant**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}`
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
@ -139,9 +148,45 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `last_comment_id`: 999999999999 (optional)
|
- `last_comment_id`: 999999999999 (optional)
|
||||||
- `links`: 0 (optional)
|
- `links`: 0 (optional)
|
||||||
- **Response**: JSON with `rant` (text, tags)
|
- **Response**: JSON with `rant` (text, tags), `comments`, `success`, `subscribed`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rant": {
|
||||||
|
"id": 18960811,
|
||||||
|
"text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.",
|
||||||
|
"score": 3,
|
||||||
|
"created_time": 1754065322,
|
||||||
|
"attached_image": "",
|
||||||
|
"num_comments": 10,
|
||||||
|
"tags": [
|
||||||
|
"too-much",
|
||||||
|
"qualifications",
|
||||||
|
"job-hunting"
|
||||||
|
],
|
||||||
|
"vote_state": 0,
|
||||||
|
"edited": false,
|
||||||
|
"link": "rants/18960811/you-know-im-getting-tired-of-this-hr-speak-in-job-applications-specifically-this",
|
||||||
|
"rt": 1,
|
||||||
|
"rc": 1,
|
||||||
|
"user_id": 1366654,
|
||||||
|
"user_username": "CaptainRant",
|
||||||
|
"user_score": 4179,
|
||||||
|
"user_avatar": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg"
|
||||||
|
},
|
||||||
|
"user_avatar_lg": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"comments": [],
|
||||||
|
"success": true,
|
||||||
|
"subscribed": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Retrieves a specific rant by ID.
|
- **Description**: Retrieves a specific rant by ID.
|
||||||
|
|
||||||
2. **Post Rant**
|
2. **Post Rant**
|
||||||
- **URL**: `/api/devrant/rants`
|
- **URL**: `/api/devrant/rants`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -153,8 +198,14 @@ TODO: document responses.
|
|||||||
- `type`: Rant type ID
|
- `type`: Rant type ID
|
||||||
- `image`: Optional image file (img/gif)
|
- `image`: Optional image file (img/gif)
|
||||||
- **Response**: JSON with `success`, `rant_id`, or `error`
|
- **Response**: JSON with `success`, `rant_id`, or `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "It looks like you just posted this same rant! Your connection might have timed out while posting so you might have seen an error, but sometimes the rant still gets posted and in this case it seems it did, so please check :) If this was not the case please contact info@devrant.io. Thanks!"
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Creates a new rant.
|
- **Description**: Creates a new rant.
|
||||||
|
|
||||||
3. **Edit Rant**
|
3. **Edit Rant**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}`
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -165,17 +216,29 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`
|
- `token_id`, `token_key`, `user_id`
|
||||||
- `image`: Optional image file
|
- `image`: Optional image file
|
||||||
- **Response**: JSON with `success` or `fail_reason`
|
- **Response**: JSON with `success` or `fail_reason`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"fail_reason": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Updates an existing rant.
|
- **Description**: Updates an existing rant.
|
||||||
|
|
||||||
4. **Delete Rant**
|
4. **Delete Rant**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}`
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
- **Method**: DELETE
|
- **Method**: DELETE
|
||||||
- **Parameters**:
|
- **Parameters**:
|
||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success` or `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "An unknown error occurred and this rant can't be deleted. Please contact support@devrant.com for help with this."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Deletes a rant.
|
- **Description**: Deletes a rant.
|
||||||
|
|
||||||
5. **Vote on Rant**
|
5. **Vote on Rant**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}/vote`
|
- **URL**: `/api/devrant/rants/{rant_id}/vote`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -184,9 +247,43 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
||||||
- `reason`: Downvote reason ID (required for downvote)
|
- `reason`: Downvote reason ID (required for downvote)
|
||||||
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
- **Response**: JSON with `success`, `rant`, or `confirmed` (false if unverified)
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"rant": {
|
||||||
|
"id": 18960811,
|
||||||
|
"text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.",
|
||||||
|
"score": 3,
|
||||||
|
"created_time": 1754065322,
|
||||||
|
"attached_image": "",
|
||||||
|
"num_comments": 10,
|
||||||
|
"tags": [
|
||||||
|
"rant",
|
||||||
|
"too-much",
|
||||||
|
"qualifications",
|
||||||
|
"job-hunting"
|
||||||
|
],
|
||||||
|
"vote_state": -1,
|
||||||
|
"edited": false,
|
||||||
|
"rt": 1,
|
||||||
|
"rc": 1,
|
||||||
|
"user_id": 1366654,
|
||||||
|
"user_username": "CaptainRant",
|
||||||
|
"user_score": 4180,
|
||||||
|
"user_avatar": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg"
|
||||||
|
},
|
||||||
|
"user_avatar_lg": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Votes on a rant.
|
- **Description**: Votes on a rant.
|
||||||
|
|
||||||
6. **Favorite/Unfavorite Rant**
|
6. **Favorite/Unfavorite Rant**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}/{favorite|unfavorite}`
|
- **URL**: `/api/devrant/rants/{rant_id}/{favorite|unfavorite}`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -194,8 +291,13 @@ TODO: document responses.
|
|||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Favorites or unfavorites a rant.
|
- **Description**: Favorites or unfavorites a rant.
|
||||||
|
|
||||||
7. **Get Rant Feed**
|
7. **Get Rant Feed**
|
||||||
- **URL**: `/api/devrant/rants`
|
- **URL**: `/api/devrant/rants`
|
||||||
- **Method**: GET
|
- **Method**: GET
|
||||||
@ -203,9 +305,66 @@ TODO: document responses.
|
|||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `ids`: JSON string of IDs (optional)
|
- `ids`: JSON string of IDs (optional)
|
||||||
- **Response**: JSON with `success`, `num_notifs`
|
- **Response**: JSON with `success`, `rants`, `settings`, `set`, `wrw`, `dpp`, `num_notifs`, `unread`, `news`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"rants": [
|
||||||
|
{
|
||||||
|
"id": 18960811,
|
||||||
|
"text": "You know, I'm getting tired of this HR-speak in job applications, specifically this:\n\n- [tech] has no secrets for you\n\nWhat, really? So I am the undisputed and absolute expert of - let's say - JavaScript? Do you know how long it takes to master that so that it holds no secrets? It even holds secrets to decade-long experts! The same goes for most other technologies in software development.\n\nSigh. Hhhhh. Ree.",
|
||||||
|
"score": 3,
|
||||||
|
"created_time": 1754065322,
|
||||||
|
"attached_image": "",
|
||||||
|
"num_comments": 9,
|
||||||
|
"tags": [
|
||||||
|
"rant",
|
||||||
|
"too-much",
|
||||||
|
"qualifications",
|
||||||
|
"job-hunting"
|
||||||
|
],
|
||||||
|
"vote_state": 0,
|
||||||
|
"edited": false,
|
||||||
|
"rt": 1,
|
||||||
|
"rc": 1,
|
||||||
|
"user_id": 1366654,
|
||||||
|
"user_username": "CaptainRant",
|
||||||
|
"user_score": 4179,
|
||||||
|
"user_avatar": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-3_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.jpg"
|
||||||
|
},
|
||||||
|
"user_avatar_lg": {
|
||||||
|
"b": "2a8b9d",
|
||||||
|
"i": "v-37_c-1_b-4_g-m_9-1_1-2_16-6_3-1_8-1_7-1_5-2_12-2_6-3_10-1_2-10_22-2_11-2_18-1_19-3_4-2_20-1_21-2.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ... more rants
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"notif_state": -1,
|
||||||
|
"notif_token": ""
|
||||||
|
},
|
||||||
|
"set": "688e90b7cf77f",
|
||||||
|
"wrw": 385,
|
||||||
|
"dpp": 0,
|
||||||
|
"num_notifs": 0,
|
||||||
|
"unread": {
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"news": {
|
||||||
|
"id": 356,
|
||||||
|
"type": "intlink",
|
||||||
|
"headline": "Weekly Group Rant",
|
||||||
|
"body": "Tips for staying productive?",
|
||||||
|
"footer": "Add tag 'wk247' to your rant",
|
||||||
|
"height": 100,
|
||||||
|
"action": "grouprant"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Retrieves rant feed with notification count.
|
- **Description**: Retrieves rant feed with notification count.
|
||||||
|
|
||||||
### Comments
|
### Comments
|
||||||
1. **Get Comment**
|
1. **Get Comment**
|
||||||
- **URL**: `/api/comments/{comment_id}`
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
@ -214,9 +373,15 @@ TODO: document responses.
|
|||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `links`: 0 (optional)
|
- `links`: 0 (optional)
|
||||||
- **Response**: JSON with `comment` (body)
|
- **Response**: JSON with `comment` (body), or `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Invalid comment specified in path."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Retrieves a specific comment by ID.
|
- **Description**: Retrieves a specific comment by ID.
|
||||||
|
|
||||||
2. **Post Comment**
|
2. **Post Comment**
|
||||||
- **URL**: `/api/devrant/rants/{rant_id}/comments`
|
- **URL**: `/api/devrant/rants/{rant_id}/comments`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -226,8 +391,13 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`
|
- `token_id`, `token_key`, `user_id`
|
||||||
- `image`: Optional image file (img/gif)
|
- `image`: Optional image file (img/gif)
|
||||||
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Posts a comment on a rant.
|
- **Description**: Posts a comment on a rant.
|
||||||
|
|
||||||
3. **Edit Comment**
|
3. **Edit Comment**
|
||||||
- **URL**: `/api/comments/{comment_id}`
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -236,17 +406,29 @@ TODO: document responses.
|
|||||||
- `comment`: Comment text
|
- `comment`: Comment text
|
||||||
- `token_id`, `token_key`, `user_id`
|
- `token_id`, `token_key`, `user_id`
|
||||||
- **Response**: JSON with `success` or `fail_reason`
|
- **Response**: JSON with `success` or `fail_reason`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Invalid comment specified in path."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Updates an existing comment.
|
- **Description**: Updates an existing comment.
|
||||||
|
|
||||||
4. **Delete Comment**
|
4. **Delete Comment**
|
||||||
- **URL**: `/api/comments/{comment_id}`
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
- **Method**: DELETE
|
- **Method**: DELETE
|
||||||
- **Parameters**:
|
- **Parameters**:
|
||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success` or `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Invalid comment specified in path."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Deletes a comment.
|
- **Description**: Deletes a comment.
|
||||||
|
|
||||||
5. **Vote on Comment**
|
5. **Vote on Comment**
|
||||||
- **URL**: `/api/comments/{comment_id}/vote`
|
- **URL**: `/api/comments/{comment_id}/vote`
|
||||||
- **Method**: POST
|
- **Method**: POST
|
||||||
@ -255,9 +437,15 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
||||||
- `reason`: Downvote reason ID (required for downvote)
|
- `reason`: Downvote reason ID (required for downvote)
|
||||||
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
- **Response**: JSON with `success` or `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"error": "Invalid comment specified in path."
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Votes on a comment.
|
- **Description**: Votes on a comment.
|
||||||
|
|
||||||
### Notifications
|
### Notifications
|
||||||
1. **Get Notification Feed**
|
1. **Get Notification Feed**
|
||||||
- **URL**: `/api/users/me/notif-feed`
|
- **URL**: `/api/users/me/notif-feed`
|
||||||
@ -267,9 +455,28 @@ TODO: document responses.
|
|||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- `ext_prof`: 1 (optional)
|
- `ext_prof`: 1 (optional)
|
||||||
- `last_time`: Last notification check time
|
- `last_time`: Last notification check time
|
||||||
- **Response**: JSON with `success`, `data` (items, check_time, username_map)
|
- **Response**: JSON with `success`, `data` (items, check_time, username_map, unread, num_unread)
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"items": [],
|
||||||
|
"check_time": 1754173634,
|
||||||
|
"username_map": [],
|
||||||
|
"unread": {
|
||||||
|
"all": 0,
|
||||||
|
"upvotes": 0,
|
||||||
|
"mentions": 0,
|
||||||
|
"comments": 0,
|
||||||
|
"subs": 0,
|
||||||
|
"total": 0
|
||||||
|
},
|
||||||
|
"num_unread": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Retrieves user notifications.
|
- **Description**: Retrieves user notifications.
|
||||||
|
|
||||||
2. **Clear Notifications**
|
2. **Clear Notifications**
|
||||||
- **URL**: `/api/users/me/notif-feed`
|
- **URL**: `/api/users/me/notif-feed`
|
||||||
- **Method**: DELETE
|
- **Method**: DELETE
|
||||||
@ -277,9 +484,14 @@ TODO: document responses.
|
|||||||
- `app`: 3
|
- `app`: 3
|
||||||
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
- **Response**: JSON with `success`
|
- **Response**: JSON with `success`
|
||||||
|
- **Success Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Clears user notifications.
|
- **Description**: Clears user notifications.
|
||||||
|
### External API
|
||||||
## External API
|
|
||||||
- **Beta List Signup**
|
- **Beta List Signup**
|
||||||
- **URL**: `https://www.hexicallabs.com/api/beta-list`
|
- **URL**: `https://www.hexicallabs.com/api/beta-list`
|
||||||
- **Method**: GET (JSONP)
|
- **Method**: GET (JSONP)
|
||||||
@ -287,8 +499,14 @@ TODO: document responses.
|
|||||||
- `email`: User email
|
- `email`: User email
|
||||||
- `platform`: Platform name
|
- `platform`: Platform name
|
||||||
- `app`: 3
|
- `app`: 3
|
||||||
|
- **Response**: JSON with `error`
|
||||||
|
- **Error Example**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Expecting value: line 1 column 1 (char 0)"
|
||||||
|
}
|
||||||
|
```
|
||||||
- **Description**: Signs up user for beta list (external service).
|
- **Description**: Signs up user for beta list (external service).
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- All endpoints expect `app=3` for identification.
|
- All endpoints expect `app=3` for identification.
|
||||||
- Authenticated endpoints require `dr_token` cookie with `token_id`, `token_key`, `user_id`.
|
- Authenticated endpoints require `dr_token` cookie with `token_id`, `token_key`, `user_id`.
|
||||||
@ -297,5 +515,3 @@ TODO: document responses.
|
|||||||
- Downvotes require a reason ID, prompting a modal if not provided.
|
- Downvotes require a reason ID, prompting a modal if not provided.
|
||||||
- Responses typically include `success` boolean; errors include `error` or `fail_reason`.
|
- Responses typically include `success` boolean; errors include `error` or `fail_reason`.
|
||||||
- Cookies (`dr_token`, `dr_guid`, `dr_session_start`, `dr_event_id`, `dr_feed_sort`, `dr_theme`, `dr_rants_viewed`, `dr_stickers_seen`, `rant_type_filters`, `news_seen`) manage state and preferences.
|
- Cookies (`dr_token`, `dr_guid`, `dr_session_start`, `dr_event_id`, `dr_feed_sort`, `dr_theme`, `dr_rants_viewed`, `dr_stickers_seen`, `rant_type_filters`, `news_seen`) manage state and preferences.
|
||||||
|
|
||||||
|
|
||||||
|
2053
api_test_results.json
Normal file
2053
api_test_results.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
dist/devranta-1.0.0-py3-none-any.whl
vendored
BIN
dist/devranta-1.0.0-py3-none-any.whl
vendored
Binary file not shown.
BIN
dist/devranta-1.0.0.tar.gz
vendored
BIN
dist/devranta-1.0.0.tar.gz
vendored
Binary file not shown.
BIN
dist/devranta-1.1.0-py3-none-any.whl
vendored
Normal file
BIN
dist/devranta-1.1.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
BIN
dist/devranta-1.1.0.tar.gz
vendored
Normal file
BIN
dist/devranta-1.1.0.tar.gz
vendored
Normal file
Binary file not shown.
17
examples/crawler/Makefile
Normal file
17
examples/crawler/Makefile
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.PHONY: all env install run clean
|
||||||
|
|
||||||
|
all: env install run
|
||||||
|
|
||||||
|
env:
|
||||||
|
python3 -m venv .venv
|
||||||
|
.
|
||||||
|
|
||||||
|
install:
|
||||||
|
. .venv/bin/activate && pip install -r requirements.txt
|
||||||
|
. .venv/bin/activate && pip install -e ../../.
|
||||||
|
|
||||||
|
run:
|
||||||
|
. .venv/bin/activate && python main.py
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf .venv
|
34
examples/crawler/README.md
Normal file
34
examples/crawler/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Example Crawler Project
|
||||||
|
|
||||||
|
This is a simple example crawler project. Follow the instructions below to set up and run the crawler.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Clone the repository or copy the project files to your local machine.
|
||||||
|
2. Make sure you have Python 3 installed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Open a terminal in the project directory.
|
||||||
|
2. Run `make` to set up the environment, install dependencies, and start the crawler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a virtual environment, install the package in editable mode from the parent directory, and run the main script.
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
To remove the virtual environment, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The project installs the package with `-e ../../.` to include the parent package `devranta` in editable mode.
|
||||||
|
- Ensure that the parent package is correctly set up in the directory structure.
|
||||||
|
|
||||||
|
Happy crawling!
|
214
examples/crawler/crawler.py
Normal file
214
examples/crawler/crawler.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import Set
|
||||||
|
from devranta.api import Api, Rant
|
||||||
|
from database import DatabaseManager
|
||||||
|
|
||||||
|
class DevRantCrawler:
|
||||||
|
def __init__(self, api: Api, db: DatabaseManager, rant_consumers: int, user_consumers: int):
|
||||||
|
self.api = api
|
||||||
|
self.db = db
|
||||||
|
self.rant_queue = asyncio.Queue(maxsize=1000000)
|
||||||
|
self.user_queue = asyncio.Queue(maxsize=1000000)
|
||||||
|
self.shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
|
self.num_rant_consumers = rant_consumers
|
||||||
|
self.num_user_consumers = user_consumers
|
||||||
|
|
||||||
|
self.seen_rant_ids: Set[int] = set()
|
||||||
|
self.seen_user_ids: Set[int] = set()
|
||||||
|
self.stats = {
|
||||||
|
"rants_processed": 0, "rants_added_to_db": 0,
|
||||||
|
"comments_added_to_db": 0, "users_processed": 0, "users_added_to_db": 0,
|
||||||
|
"api_errors": 0, "producer_loops": 0, "end_of_feed_hits": 0,
|
||||||
|
"rants_queued": 0, "users_queued": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _queue_user_if_new(self, user_id: int):
|
||||||
|
if user_id in self.seen_user_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.seen_user_ids.add(user_id)
|
||||||
|
if not await self.db.user_exists(user_id):
|
||||||
|
await self.user_queue.put(user_id)
|
||||||
|
self.stats["users_queued"] += 1
|
||||||
|
|
||||||
|
async def _queue_rant_if_new(self, rant_obj: Rant):
|
||||||
|
rant_id = rant_obj['id']
|
||||||
|
if rant_id in self.seen_rant_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.seen_rant_ids.add(rant_id)
|
||||||
|
if not await self.db.rant_exists(rant_id):
|
||||||
|
await self.db.add_rant(rant_obj)
|
||||||
|
self.stats["rants_added_to_db"] += 1
|
||||||
|
await self.rant_queue.put(rant_id)
|
||||||
|
self.stats["rants_queued"] += 1
|
||||||
|
|
||||||
|
async def _initial_seed(self):
|
||||||
|
logging.info("Starting initial seeder to re-ignite crawling process...")
|
||||||
|
user_ids = await self.db.get_random_user_ids(limit=2000)
|
||||||
|
if not user_ids:
|
||||||
|
logging.info("Seeder found no existing users. Crawler will start from scratch.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for user_id in user_ids:
|
||||||
|
if user_id not in self.seen_user_ids:
|
||||||
|
self.seen_user_ids.add(user_id)
|
||||||
|
await self.user_queue.put(user_id)
|
||||||
|
self.stats["users_queued"] += 1
|
||||||
|
logging.info(f"Seeder finished: Queued {len(user_ids)} users to kickstart exploration.")
|
||||||
|
|
||||||
|
async def _rant_producer(self):
|
||||||
|
logging.info("Rant producer started.")
|
||||||
|
skip = 0
|
||||||
|
consecutive_empty_responses = 0
|
||||||
|
|
||||||
|
while not self.shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
logging.info(f"Producer: Fetching rants with skip={skip}...")
|
||||||
|
rants = await self.api.get_rants(sort="recent", limit=50, skip=skip)
|
||||||
|
self.stats["producer_loops"] += 1
|
||||||
|
|
||||||
|
if not rants:
|
||||||
|
consecutive_empty_responses += 1
|
||||||
|
logging.info(f"Producer: Feed returned empty. Consecutive empty hits: {consecutive_empty_responses}.")
|
||||||
|
if consecutive_empty_responses >= 5:
|
||||||
|
self.stats["end_of_feed_hits"] += 1
|
||||||
|
logging.info("Producer: End of feed likely reached. Pausing for 15 minutes before reset.")
|
||||||
|
await asyncio.sleep(900)
|
||||||
|
skip = 0
|
||||||
|
consecutive_empty_responses = 0
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
continue
|
||||||
|
|
||||||
|
consecutive_empty_responses = 0
|
||||||
|
new_rants_found = 0
|
||||||
|
for rant in rants:
|
||||||
|
await self._queue_rant_if_new(rant)
|
||||||
|
new_rants_found += 1
|
||||||
|
|
||||||
|
logging.info(f"Producer: Processed {new_rants_found} rants from feed. Total queued: {self.stats['rants_queued']}.")
|
||||||
|
skip += len(rants)
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(f"Producer: Unhandled exception: {e}. Retrying in 60s.")
|
||||||
|
self.stats["api_errors"] += 1
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
async def _rant_consumer(self, worker_id: int):
|
||||||
|
logging.info(f"Rant consumer #{worker_id} started.")
|
||||||
|
while not self.shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
rant_id = await self.rant_queue.get()
|
||||||
|
logging.info(f"Rant consumer #{worker_id}: Processing rant ID {rant_id}.")
|
||||||
|
|
||||||
|
rant_details = await self.api.get_rant(rant_id)
|
||||||
|
if not rant_details or not rant_details.get("success"):
|
||||||
|
logging.warning(f"Rant consumer #{worker_id}: Failed to fetch details for rant {rant_id}.")
|
||||||
|
self.rant_queue.task_done()
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self._queue_user_if_new(rant_details['rant']['user_id'])
|
||||||
|
|
||||||
|
comments = rant_details.get("comments", [])
|
||||||
|
for comment in comments:
|
||||||
|
await self.db.add_comment(comment)
|
||||||
|
self.stats["comments_added_to_db"] += 1
|
||||||
|
await self._queue_user_if_new(comment['user_id'])
|
||||||
|
|
||||||
|
logging.info(f"Rant consumer #{worker_id}: Finished processing rant {rant_id}, found {len(comments)} comments.")
|
||||||
|
self.stats["rants_processed"] += 1
|
||||||
|
self.rant_queue.task_done()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Rant consumer #{worker_id}: Unhandled exception: {e}")
|
||||||
|
self.rant_queue.task_done()
|
||||||
|
|
||||||
|
async def _user_consumer(self, worker_id: int):
|
||||||
|
logging.info(f"User consumer #{worker_id} started.")
|
||||||
|
while not self.shutdown_event.is_set():
|
||||||
|
try:
|
||||||
|
user_id = await self.user_queue.get()
|
||||||
|
logging.info(f"User consumer #{worker_id}: Processing user ID {user_id}.")
|
||||||
|
|
||||||
|
profile = await self.api.get_profile(user_id)
|
||||||
|
if not profile:
|
||||||
|
logging.warning(f"User consumer #{worker_id}: Could not fetch profile for user {user_id}.")
|
||||||
|
self.user_queue.task_done()
|
||||||
|
continue
|
||||||
|
|
||||||
|
await self.db.add_user(profile, user_id)
|
||||||
|
self.stats["users_added_to_db"] += 1
|
||||||
|
|
||||||
|
rants_found_on_profile = 0
|
||||||
|
content_sections = profile.get("content", {}).get("content", {})
|
||||||
|
for section_name in ["rants", "upvoted", "favorites"]:
|
||||||
|
for rant_obj in content_sections.get(section_name, []):
|
||||||
|
await self._queue_rant_if_new(rant_obj)
|
||||||
|
rants_found_on_profile += 1
|
||||||
|
|
||||||
|
logging.info(f"User consumer #{worker_id}: Finished user {user_id}, found and queued {rants_found_on_profile} associated rants.")
|
||||||
|
self.stats["users_processed"] += 1
|
||||||
|
self.user_queue.task_done()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"User consumer #{worker_id}: Unhandled exception: {e}")
|
||||||
|
self.user_queue.task_done()
|
||||||
|
|
||||||
|
async def _stats_reporter(self):
|
||||||
|
logging.info("Stats reporter started.")
|
||||||
|
while not self.shutdown_event.is_set():
|
||||||
|
await asyncio.sleep(15)
|
||||||
|
logging.info(
|
||||||
|
f"[STATS] Rants Q'd/Proc: {self.stats['rants_queued']}/{self.stats['rants_processed']} | "
|
||||||
|
f"Users Q'd/Proc: {self.stats['users_queued']}/{self.stats['users_processed']} | "
|
||||||
|
f"Comments DB: {self.stats['comments_added_to_db']} | "
|
||||||
|
f"Queues (R/U): {self.rant_queue.qsize()}/{self.user_queue.qsize()} | "
|
||||||
|
f"API Errors: {self.stats['api_errors']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
logging.info("Exhaustive crawler starting...")
|
||||||
|
await self._initial_seed()
|
||||||
|
|
||||||
|
logging.info("Starting main producer and consumer tasks...")
|
||||||
|
tasks = []
|
||||||
|
try:
|
||||||
|
tasks.append(asyncio.create_task(self._rant_producer()))
|
||||||
|
tasks.append(asyncio.create_task(self._stats_reporter()))
|
||||||
|
|
||||||
|
for i in range(self.num_rant_consumers):
|
||||||
|
tasks.append(asyncio.create_task(self._rant_consumer(i + 1)))
|
||||||
|
|
||||||
|
for i in range(self.num_user_consumers):
|
||||||
|
tasks.append(asyncio.create_task(self._user_consumer(i + 1)))
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logging.info("Crawler run cancelled.")
|
||||||
|
finally:
|
||||||
|
await self.shutdown()
|
||||||
|
|
||||||
|
async def shutdown(self):
|
||||||
|
if self.shutdown_event.is_set():
|
||||||
|
return
|
||||||
|
logging.info("Shutting down... sending signal to all tasks.")
|
||||||
|
self.shutdown_event.set()
|
||||||
|
|
||||||
|
logging.info("Waiting for queues to empty... Press Ctrl+C again to force exit.")
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.rant_queue.join(), timeout=30)
|
||||||
|
await asyncio.wait_for(self.user_queue.join(), timeout=30)
|
||||||
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
||||||
|
logging.warning("Could not empty queues in time, proceeding with shutdown.")
|
||||||
|
|
||||||
|
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
|
||||||
|
for task in tasks:
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
logging.info("All tasks cancelled.")
|
||||||
|
logging.info(f"--- FINAL STATS ---\n{self.stats}")
|
||||||
|
|
96
examples/crawler/database.py
Normal file
96
examples/crawler/database.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import logging
|
||||||
|
import aiosqlite
|
||||||
|
from typing import List
|
||||||
|
from devranta.api import Rant, Comment, UserProfile
|
||||||
|
|
||||||
|
class DatabaseManager:
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
self.db_path = db_path
|
||||||
|
self._conn: aiosqlite.Connection | None = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
logging.info(f"Connecting to database at {self.db_path}...")
|
||||||
|
self._conn = await aiosqlite.connect(self.db_path)
|
||||||
|
await self._conn.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
await self._conn.execute("PRAGMA foreign_keys=ON;")
|
||||||
|
await self.create_tables()
|
||||||
|
logging.info("Database connection successful.")
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self._conn:
|
||||||
|
await self._conn.close()
|
||||||
|
logging.info("Database connection closed.")
|
||||||
|
|
||||||
|
async def create_tables(self):
|
||||||
|
logging.info("Ensuring database tables exist...")
|
||||||
|
await self._conn.executescript("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
score INTEGER,
|
||||||
|
about TEXT,
|
||||||
|
location TEXT,
|
||||||
|
created_time INTEGER,
|
||||||
|
skills TEXT,
|
||||||
|
github TEXT,
|
||||||
|
website TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS rants (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
user_id INTEGER,
|
||||||
|
text TEXT,
|
||||||
|
score INTEGER,
|
||||||
|
created_time INTEGER,
|
||||||
|
num_comments INTEGER
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
rant_id INTEGER,
|
||||||
|
user_id INTEGER,
|
||||||
|
body TEXT,
|
||||||
|
score INTEGER,
|
||||||
|
created_time INTEGER
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
await self._conn.commit()
|
||||||
|
logging.info("Table schema verified.")
|
||||||
|
|
||||||
|
async def add_rant(self, rant: Rant):
|
||||||
|
await self._conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO rants (id, user_id, text, score, created_time, num_comments) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(rant['id'], rant['user_id'], rant['text'], rant['score'], rant['created_time'], rant['num_comments'])
|
||||||
|
)
|
||||||
|
await self._conn.commit()
|
||||||
|
|
||||||
|
async def add_comment(self, comment: Comment):
|
||||||
|
await self._conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO comments (id, rant_id, user_id, body, score, created_time) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
(comment['id'], comment['rant_id'], comment['user_id'], comment['body'], comment['score'], comment['created_time'])
|
||||||
|
)
|
||||||
|
await self._conn.commit()
|
||||||
|
|
||||||
|
async def add_user(self, user: UserProfile, user_id: int):
|
||||||
|
await self._conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO users (id, username, score, about, location, created_time, skills, github, website) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
(user_id, user['username'], user['score'], user['about'], user['location'], user['created_time'], user['skills'], user['github'], user['website'])
|
||||||
|
)
|
||||||
|
await self._conn.commit()
|
||||||
|
|
||||||
|
async def rant_exists(self, rant_id: int) -> bool:
|
||||||
|
async with self._conn.execute("SELECT 1 FROM rants WHERE id = ? LIMIT 1", (rant_id,)) as cursor:
|
||||||
|
return await cursor.fetchone() is not None
|
||||||
|
|
||||||
|
async def user_exists(self, user_id: int) -> bool:
|
||||||
|
async with self._conn.execute("SELECT 1 FROM users WHERE id = ? LIMIT 1", (user_id,)) as cursor:
|
||||||
|
return await cursor.fetchone() is not None
|
||||||
|
|
||||||
|
async def get_random_user_ids(self, limit: int) -> List[int]:
|
||||||
|
logging.info(f"Fetching up to {limit} random user IDs from database for seeding...")
|
||||||
|
query = "SELECT id FROM users ORDER BY RANDOM() LIMIT ?"
|
||||||
|
async with self._conn.execute(query, (limit,)) as cursor:
|
||||||
|
rows = await cursor.fetchall()
|
||||||
|
user_ids = [row[0] for row in rows]
|
||||||
|
logging.info(f"Found {len(user_ids)} user IDs to seed.")
|
||||||
|
return user_ids
|
||||||
|
|
46
examples/crawler/main.py
Normal file
46
examples/crawler/main.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# main.py
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import signal
|
||||||
|
|
||||||
|
from devranta.api import Api
|
||||||
|
from database import DatabaseManager
|
||||||
|
from crawler import DevRantCrawler
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
DB_FILE = "devrant.sqlite"
|
||||||
|
CONCURRENT_RANT_CONSUMERS = 10 # How many rants to process at once
|
||||||
|
CONCURRENT_USER_CONSUMERS = 5 # How many user profiles to fetch at once
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Initializes and runs the crawler."""
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] - %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
|
api = Api()
|
||||||
|
|
||||||
|
async with DatabaseManager(DB_FILE) as db:
|
||||||
|
crawler = DevRantCrawler(
|
||||||
|
api=api,
|
||||||
|
db=db,
|
||||||
|
rant_consumers=CONCURRENT_RANT_CONSUMERS,
|
||||||
|
user_consumers=CONCURRENT_USER_CONSUMERS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up a signal handler for graceful shutdown on Ctrl+C
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(
|
||||||
|
sig, lambda s=sig: asyncio.create_task(crawler.shutdown())
|
||||||
|
)
|
||||||
|
|
||||||
|
await crawler.run()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info("Main loop interrupted. Exiting.")
|
1
examples/crawler/requirements.txt
Normal file
1
examples/crawler/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
aiosqlite
|
@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = devranta
|
name = devranta
|
||||||
version = 1.0.0
|
version = 1.1.0
|
||||||
description = Async devRant API client made with aiohttp.
|
description = Async devRant API client made with aiohttp.
|
||||||
author = retoor
|
author = retoor
|
||||||
author_email = retoor@molodetz.nl
|
author_email = retoor@molodetz.nl
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
Metadata-Version: 2.1
|
Metadata-Version: 2.4
|
||||||
Name: devranta
|
Name: devranta
|
||||||
Version: 1.0.0
|
Version: 1.1.0
|
||||||
Summary: Async devRant API client made with aiohttp.
|
Summary: Async devRant API client made with aiohttp.
|
||||||
Author: retoor
|
Author: retoor
|
||||||
Author-email: retoor@molodetz.nl
|
Author-email: retoor@molodetz.nl
|
||||||
@ -13,10 +13,7 @@ Requires-Dist: dataset
|
|||||||
|
|
||||||
# devRanta
|
# devRanta
|
||||||
|
|
||||||
devRanta is an async devrant client written in and for Python.
|
devRanta is the best async devRant client written in Python. Authentication is only needed for half of the functionality; thus, the username and password are optional parameters when constructing the main class of this package (Api). You can find the latest packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages).
|
||||||
Authentication is only needed for half of the functionality and thus username and password are optional parameters by constructing the main class of this package (Api).
|
|
||||||
|
|
||||||
You can find last packages in tar and wheel format [here](https://retoor.molodetz.nl/retoor/devranta/packages).
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
```
|
```
|
||||||
@ -44,3 +41,274 @@ async def list_rants():
|
|||||||
See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use.
|
See [tests](src/devranta/tests.py) for [examples](src/devranta/tests.py) on how to use.
|
||||||
|
|
||||||
|
|
||||||
|
# devRant API Documentation
|
||||||
|
|
||||||
|
For people wanting to build their own client.
|
||||||
|
|
||||||
|
TODO: document responses.
|
||||||
|
|
||||||
|
## Base URL
|
||||||
|
`https://devrant.com/api`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
- Uses `dr_token` cookie with `token_id`, `token_key`, and `user_id`.
|
||||||
|
- Required for endpoints needing user authentication.
|
||||||
|
- `guid`, `plat`, `sid`, `seid` included in requests for session tracking.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### User Management
|
||||||
|
1. **Register User**
|
||||||
|
- **URL**: `/api/users`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3 (constant)
|
||||||
|
- `type`: 1 (constant)
|
||||||
|
- `email`: User email
|
||||||
|
- `username`: User username
|
||||||
|
- `password`: User password
|
||||||
|
- `guid`: Unique identifier (from `getMyGuid`)
|
||||||
|
- `plat`: 3 (constant)
|
||||||
|
- `sid`: Session start time (from `getSessionStartTime`)
|
||||||
|
- `seid`: Session event ID (from `getSessionEventId`)
|
||||||
|
- **Response**: JSON with `success`, `auth_token`, or `error` and `error_field`
|
||||||
|
- **Description**: Creates a new user account.
|
||||||
|
|
||||||
|
2. **Login User**
|
||||||
|
- **URL**: `/api/users/auth-token`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `username`: User username
|
||||||
|
- `password`: User password
|
||||||
|
- `guid`: Unique identifier
|
||||||
|
- `plat`: 3
|
||||||
|
- `sid`: Session start time
|
||||||
|
- `seid`: Session event ID
|
||||||
|
- **Response**: JSON with `success`, `auth_token`, or `error`
|
||||||
|
- **Description**: Authenticates user and returns auth token.
|
||||||
|
|
||||||
|
3. **Edit Profile**
|
||||||
|
- **URL**: `/api/users/me/edit-profile`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`: Token ID
|
||||||
|
- `token_key`: Token key
|
||||||
|
- `user_id`: User ID
|
||||||
|
- `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `profile_about`: User bio
|
||||||
|
- `profile_skills`: User skills
|
||||||
|
- `profile_location`: User location
|
||||||
|
- `profile_website`: User website
|
||||||
|
- `profile_github`: GitHub username
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Updates user profile information.
|
||||||
|
|
||||||
|
4. **Forgot Password**
|
||||||
|
- **URL**: `/api/users/forgot-password`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `username`: User username
|
||||||
|
- `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Initiates password reset process.
|
||||||
|
|
||||||
|
5. **Resend Confirmation Email**
|
||||||
|
- **URL**: `/api/users/me/resend-confirm`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Resends account confirmation email.
|
||||||
|
|
||||||
|
6. **Delete Account**
|
||||||
|
- **URL**: `/api/users/me`
|
||||||
|
- **Method**: DELETE
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Deletes user account.
|
||||||
|
|
||||||
|
7. **Mark News as Read**
|
||||||
|
- **URL**: `/api/users/me/mark-news-read`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `news_id`: News item ID
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Marks a news item as read for logged-in users.
|
||||||
|
|
||||||
|
### Rants
|
||||||
|
1. **Get Rant**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
|
- **Method**: GET
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `last_comment_id`: 999999999999 (optional)
|
||||||
|
- `links`: 0 (optional)
|
||||||
|
- **Response**: JSON with `rant` (text, tags)
|
||||||
|
- **Description**: Retrieves a specific rant by ID.
|
||||||
|
|
||||||
|
2. **Post Rant**
|
||||||
|
- **URL**: `/api/devrant/rants`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters** (FormData):
|
||||||
|
- `app`: 3
|
||||||
|
- `rant`: Rant text
|
||||||
|
- `tags`: Comma-separated tags
|
||||||
|
- `token_id`, `token_key`, `user_id`
|
||||||
|
- `type`: Rant type ID
|
||||||
|
- `image`: Optional image file (img/gif)
|
||||||
|
- **Response**: JSON with `success`, `rant_id`, or `error`
|
||||||
|
- **Description**: Creates a new rant.
|
||||||
|
|
||||||
|
3. **Edit Rant**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters** (FormData):
|
||||||
|
- `app`: 3
|
||||||
|
- `rant`: Rant text
|
||||||
|
- `tags`: Comma-separated tags
|
||||||
|
- `token_id`, `token_key`, `user_id`
|
||||||
|
- `image`: Optional image file
|
||||||
|
- **Response**: JSON with `success` or `fail_reason`
|
||||||
|
- **Description**: Updates an existing rant.
|
||||||
|
|
||||||
|
4. **Delete Rant**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}`
|
||||||
|
- **Method**: DELETE
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Deletes a rant.
|
||||||
|
|
||||||
|
5. **Vote on Rant**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}/vote`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
||||||
|
- `reason`: Downvote reason ID (required for downvote)
|
||||||
|
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
||||||
|
- **Description**: Votes on a rant.
|
||||||
|
|
||||||
|
6. **Favorite/Unfavorite Rant**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}/{favorite|unfavorite}`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Favorites or unfavorites a rant.
|
||||||
|
|
||||||
|
7. **Get Rant Feed**
|
||||||
|
- **URL**: `/api/devrant/rants`
|
||||||
|
- **Method**: GET
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `ids`: JSON string of IDs (optional)
|
||||||
|
- **Response**: JSON with `success`, `num_notifs`
|
||||||
|
- **Description**: Retrieves rant feed with notification count.
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
1. **Get Comment**
|
||||||
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
|
- **Method**: GET
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `links`: 0 (optional)
|
||||||
|
- **Response**: JSON with `comment` (body)
|
||||||
|
- **Description**: Retrieves a specific comment by ID.
|
||||||
|
|
||||||
|
2. **Post Comment**
|
||||||
|
- **URL**: `/api/devrant/rants/{rant_id}/comments`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters** (FormData):
|
||||||
|
- `app`: 3
|
||||||
|
- `comment`: Comment text
|
||||||
|
- `token_id`, `token_key`, `user_id`
|
||||||
|
- `image`: Optional image file (img/gif)
|
||||||
|
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
||||||
|
- **Description**: Posts a comment on a rant.
|
||||||
|
|
||||||
|
3. **Edit Comment**
|
||||||
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters** (FormData):
|
||||||
|
- `app`: 3
|
||||||
|
- `comment`: Comment text
|
||||||
|
- `token_id`, `token_key`, `user_id`
|
||||||
|
- **Response**: JSON with `success` or `fail_reason`
|
||||||
|
- **Description**: Updates an existing comment.
|
||||||
|
|
||||||
|
4. **Delete Comment**
|
||||||
|
- **URL**: `/api/comments/{comment_id}`
|
||||||
|
- **Method**: DELETE
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Deletes a comment.
|
||||||
|
|
||||||
|
5. **Vote on Comment**
|
||||||
|
- **URL**: `/api/comments/{comment_id}/vote`
|
||||||
|
- **Method**: POST
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `vote`: 1 (upvote), -1 (downvote), 0 (remove vote)
|
||||||
|
- `reason`: Downvote reason ID (required for downvote)
|
||||||
|
- **Response**: JSON with `success` or `confirmed` (false if unverified)
|
||||||
|
- **Description**: Votes on a comment.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
1. **Get Notification Feed**
|
||||||
|
- **URL**: `/api/users/me/notif-feed`
|
||||||
|
- **Method**: GET
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- `ext_prof`: 1 (optional)
|
||||||
|
- `last_time`: Last notification check time
|
||||||
|
- **Response**: JSON with `success`, `data` (items, check_time, username_map)
|
||||||
|
- **Description**: Retrieves user notifications.
|
||||||
|
|
||||||
|
2. **Clear Notifications**
|
||||||
|
- **URL**: `/api/users/me/notif-feed`
|
||||||
|
- **Method**: DELETE
|
||||||
|
- **Parameters**:
|
||||||
|
- `app`: 3
|
||||||
|
- `token_id`, `token_key`, `user_id`, `guid`, `plat`, `sid`, `seid`
|
||||||
|
- **Response**: JSON with `success`
|
||||||
|
- **Description**: Clears user notifications.
|
||||||
|
|
||||||
|
## External API
|
||||||
|
- **Beta List Signup**
|
||||||
|
- **URL**: `https://www.hexicallabs.com/api/beta-list`
|
||||||
|
- **Method**: GET (JSONP)
|
||||||
|
- **Parameters**:
|
||||||
|
- `email`: User email
|
||||||
|
- `platform`: Platform name
|
||||||
|
- `app`: 3
|
||||||
|
- **Description**: Signs up user for beta list (external service).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- All endpoints expect `app=3` for identification.
|
||||||
|
- Authenticated endpoints require `dr_token` cookie with `token_id`, `token_key`, `user_id`.
|
||||||
|
- `guid`, `plat`, `sid`, `seid` are used for session tracking.
|
||||||
|
- Image uploads use FormData for rants and comments.
|
||||||
|
- Downvotes require a reason ID, prompting a modal if not provided.
|
||||||
|
- Responses typically include `success` boolean; errors include `error` or `fail_reason`.
|
||||||
|
- Cookies (`dr_token`, `dr_guid`, `dr_session_start`, `dr_event_id`, `dr_feed_sort`, `dr_theme`, `dr_rants_viewed`, `dr_stickers_seen`, `rant_type_filters`, `news_seen`) manage state and preferences.
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ setup.cfg
|
|||||||
src/devranta/__init__.py
|
src/devranta/__init__.py
|
||||||
src/devranta/__main__.py
|
src/devranta/__main__.py
|
||||||
src/devranta/api.py
|
src/devranta/api.py
|
||||||
|
src/devranta/api_plain.py
|
||||||
|
src/devranta/api_requests.py
|
||||||
src/devranta/tests.py
|
src/devranta/tests.py
|
||||||
src/devranta.egg-info/PKG-INFO
|
src/devranta.egg-info/PKG-INFO
|
||||||
src/devranta.egg-info/SOURCES.txt
|
src/devranta.egg-info/SOURCES.txt
|
||||||
|
@ -29,8 +29,8 @@ class Image(TypedDict):
|
|||||||
height: int
|
height: int
|
||||||
|
|
||||||
class UserAvatar(TypedDict):
|
class UserAvatar(TypedDict):
|
||||||
b: str # background color
|
b: str # background color
|
||||||
i: NotRequired[str] # image identifier
|
i: Optional[str] # image identifier
|
||||||
|
|
||||||
class Rant(TypedDict):
|
class Rant(TypedDict):
|
||||||
id: int
|
id: int
|
||||||
@ -81,7 +81,7 @@ class Notification(TypedDict):
|
|||||||
comment_id: int
|
comment_id: int
|
||||||
created_time: int
|
created_time: int
|
||||||
read: int
|
read: int
|
||||||
uid: int # User ID of the notifier
|
uid: int # User ID of the notifier
|
||||||
username: str
|
username: str
|
||||||
|
|
||||||
# --- API Class ---
|
# --- API Class ---
|
||||||
@ -162,7 +162,7 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not self.username or not self.password:
|
if not self.username or not self.password:
|
||||||
raise Exception("No authentication details supplied.")
|
raise Exception("No authentication details supplied.")
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url("users/auth-token"),
|
url=self.patch_url("users/auth-token"),
|
||||||
data={
|
data={
|
||||||
@ -180,7 +180,7 @@ class Api:
|
|||||||
self.user_id = self.auth.get("user_id")
|
self.user_id = self.auth.get("user_id")
|
||||||
self.token_id = self.auth.get("id")
|
self.token_id = self.auth.get("id")
|
||||||
self.token_key = self.auth.get("key")
|
self.token_key = self.auth.get("key")
|
||||||
return bool(self.auth)
|
return bool(self.auth)
|
||||||
|
|
||||||
async def ensure_login(self) -> bool:
|
async def ensure_login(self) -> bool:
|
||||||
"""Ensures the user is logged in before making a request."""
|
"""Ensures the user is logged in before making a request."""
|
||||||
@ -188,17 +188,6 @@ class Api:
|
|||||||
return await self.login()
|
return await self.login()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def __aenter__(self) -> aiohttp.ClientSession:
|
|
||||||
"""Asynchronous context manager entry."""
|
|
||||||
self.session = aiohttp.ClientSession()
|
|
||||||
return self.session
|
|
||||||
|
|
||||||
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
"""Asynchronous context manager exit."""
|
|
||||||
if self.session and not self.session.closed:
|
|
||||||
await self.session.close()
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
async def register_user(self, email: str, username: str, password: str) -> bool:
|
async def register_user(self, email: str, username: str, password: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Registers a new user.
|
Registers a new user.
|
||||||
@ -220,8 +209,7 @@ class Api:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
response = None
|
async with aiohttp.ClientSession() as session:
|
||||||
async with self as session:
|
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"users"),
|
url=self.patch_url(f"users"),
|
||||||
data=self.patch_auth({
|
data=self.patch_auth({
|
||||||
@ -231,10 +219,8 @@ class Api:
|
|||||||
"plat": 3
|
"plat": 3
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
if not response:
|
obj = await response.json()
|
||||||
return False
|
return obj.get('success', False)
|
||||||
obj = await response.json()
|
|
||||||
return obj.get('success', False)
|
|
||||||
|
|
||||||
async def get_comments_from_user(self, username: str) -> List[Comment]:
|
async def get_comments_from_user(self, username: str) -> List[Comment]:
|
||||||
"""
|
"""
|
||||||
@ -244,8 +230,7 @@ class Api:
|
|||||||
username (str): The username of the user.
|
username (str): The username of the user.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Comment]: A list of comment objects. The structure of each comment
|
List[Comment]: A list of comment objects.
|
||||||
is inferred from the general API design.
|
|
||||||
"""
|
"""
|
||||||
user_id = await self.get_user_id(username)
|
user_id = await self.get_user_id(username)
|
||||||
if not user_id:
|
if not user_id:
|
||||||
@ -253,7 +238,6 @@ class Api:
|
|||||||
profile = await self.get_profile(user_id)
|
profile = await self.get_profile(user_id)
|
||||||
if not profile:
|
if not profile:
|
||||||
return []
|
return []
|
||||||
# The API nests content twice
|
|
||||||
return profile.get("content", {}).get("content", {}).get("comments", [])
|
return profile.get("content", {}).get("content", {}).get("comments", [])
|
||||||
|
|
||||||
async def post_comment(self, rant_id: int, comment: str) -> bool:
|
async def post_comment(self, rant_id: int, comment: str) -> bool:
|
||||||
@ -269,13 +253,13 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return False
|
return False
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"devrant/rants/{rant_id}/comments"),
|
url=self.patch_url(f"devrant/rants/{rant_id}/comments"),
|
||||||
data=self.patch_auth({"comment": comment, "plat": 2}),
|
data=self.patch_auth({"comment": comment, "plat": 2}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def get_comment(self, id_: int) -> Optional[Comment]:
|
async def get_comment(self, id_: int) -> Optional[Comment]:
|
||||||
"""
|
"""
|
||||||
@ -287,16 +271,12 @@ class Api:
|
|||||||
Returns:
|
Returns:
|
||||||
Optional[Comment]: A dictionary representing the comment, or None if not found.
|
Optional[Comment]: A dictionary representing the comment, or None if not found.
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url(f"comments/{id_}"), params=self.patch_auth()
|
url=self.patch_url(f"comments/{id_}"), params=self.patch_auth()
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
|
return obj.get("comment") if obj.get("success") else None
|
||||||
if not obj.get("success"):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return obj.get("comment")
|
|
||||||
|
|
||||||
async def delete_comment(self, id_: int) -> bool:
|
async def delete_comment(self, id_: int) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -310,12 +290,12 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return False
|
return False
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.delete(
|
response = await session.delete(
|
||||||
url=self.patch_url(f"comments/{id_}"), params=self.patch_auth()
|
url=self.patch_url(f"comments/{id_}"), params=self.patch_auth()
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def get_profile(self, id_: int) -> Optional[UserProfile]:
|
async def get_profile(self, id_: int) -> Optional[UserProfile]:
|
||||||
"""
|
"""
|
||||||
@ -326,38 +306,13 @@ class Api:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[UserProfile]: A dictionary with the user's profile data.
|
Optional[UserProfile]: A dictionary with the user's profile data.
|
||||||
|
|
||||||
Profile Structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "string",
|
|
||||||
"score": int,
|
|
||||||
"about": "string",
|
|
||||||
"location": "string",
|
|
||||||
"created_time": int,
|
|
||||||
"skills": "string",
|
|
||||||
"github": "string",
|
|
||||||
"website": "string",
|
|
||||||
"avatar": { "b": "hex_color", "i": "image_id" },
|
|
||||||
"content": {
|
|
||||||
"content": {
|
|
||||||
"rants": [ RantObject, ... ],
|
|
||||||
"upvoted": [ RantObject, ... ],
|
|
||||||
"comments": [ CommentObject, ... ],
|
|
||||||
"favorites": [ RantObject, ... ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url(f"users/{id_}"), params=self.patch_auth()
|
url=self.patch_url(f"users/{id_}"), params=self.patch_auth()
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
if not obj.get("success"):
|
return obj.get("profile") if obj.get("success") else None
|
||||||
return None
|
|
||||||
return obj.get("profile")
|
|
||||||
|
|
||||||
async def search(self, term: str) -> List[Rant]:
|
async def search(self, term: str) -> List[Rant]:
|
||||||
"""
|
"""
|
||||||
@ -369,15 +324,13 @@ class Api:
|
|||||||
Returns:
|
Returns:
|
||||||
List[Rant]: A list of rant objects from the search results.
|
List[Rant]: A list of rant objects from the search results.
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url("devrant/search"),
|
url=self.patch_url("devrant/search"),
|
||||||
params=self.patch_auth({"term": term}),
|
params=self.patch_auth({"term": term}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
if not obj.get("success"):
|
return obj.get("results", []) if obj.get("success") else []
|
||||||
return []
|
|
||||||
return obj.get("results", [])
|
|
||||||
|
|
||||||
async def get_rant(self, id: int) -> Dict[str, Any]:
|
async def get_rant(self, id: int) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
@ -388,23 +341,13 @@ class Api:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: The full API response object.
|
Dict[str, Any]: The full API response object.
|
||||||
|
|
||||||
Response Structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"rant": { RantObject },
|
|
||||||
"comments": [ CommentObject, ... ],
|
|
||||||
"success": true,
|
|
||||||
"subscribed": 0 or 1
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
self.patch_url(f"devrant/rants/{id}"),
|
self.patch_url(f"devrant/rants/{id}"),
|
||||||
params=self.patch_auth(),
|
params=self.patch_auth(),
|
||||||
)
|
)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
||||||
async def get_rants(self, sort: str = "recent", limit: int = 20, skip: int = 0) -> List[Rant]:
|
async def get_rants(self, sort: str = "recent", limit: int = 20, skip: int = 0) -> List[Rant]:
|
||||||
"""
|
"""
|
||||||
@ -418,15 +361,13 @@ class Api:
|
|||||||
Returns:
|
Returns:
|
||||||
List[Rant]: A list of rant objects.
|
List[Rant]: A list of rant objects.
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url("devrant/rants"),
|
url=self.patch_url("devrant/rants"),
|
||||||
params=self.patch_auth({"sort": sort, "limit": limit, "skip": skip}),
|
params=self.patch_auth({"sort": sort, "limit": limit, "skip": skip}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
if not obj.get("success"):
|
return obj.get("rants", []) if obj.get("success") else []
|
||||||
return []
|
|
||||||
return obj.get("rants", [])
|
|
||||||
|
|
||||||
async def get_user_id(self, username: str) -> Optional[int]:
|
async def get_user_id(self, username: str) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
@ -437,26 +378,15 @@ class Api:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[int]: The user's ID, or None if not found.
|
Optional[int]: The user's ID, or None if not found.
|
||||||
|
|
||||||
Response Structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"user_id": int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url("get-user-id"),
|
url=self.patch_url("get-user-id"),
|
||||||
params=self.patch_auth({"username": username}),
|
params=self.patch_auth({"username": username}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
if not obj.get("success"):
|
return obj.get("user_id") if obj.get("success") else None
|
||||||
return None
|
|
||||||
return obj.get("user_id")
|
|
||||||
|
|
||||||
@property
|
|
||||||
async def mentions(self) -> List[Notification]:
|
async def mentions(self) -> List[Notification]:
|
||||||
"""
|
"""
|
||||||
Fetches notifications where the user was mentioned.
|
Fetches notifications where the user was mentioned.
|
||||||
@ -464,7 +394,7 @@ class Api:
|
|||||||
Returns:
|
Returns:
|
||||||
List[Notification]: A list of mention notification objects.
|
List[Notification]: A list of mention notification objects.
|
||||||
"""
|
"""
|
||||||
notifications = await self.notifs
|
notifications = await self.notifs()
|
||||||
return [
|
return [
|
||||||
notif for notif in notifications if notif.get("type") == "comment_mention"
|
notif for notif in notifications if notif.get("type") == "comment_mention"
|
||||||
]
|
]
|
||||||
@ -482,13 +412,13 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return False
|
return False
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"comments/{comment_id}"),
|
url=self.patch_url(f"comments/{comment_id}"),
|
||||||
data=self.patch_auth({"comment": comment}),
|
data=self.patch_auth({"comment": comment}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def vote_rant(self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
async def vote_rant(self, rant_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -504,13 +434,13 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return False
|
return False
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
url=self.patch_url(f"devrant/rants/{rant_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
async def vote_comment(self, comment_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
async def vote_comment(self, comment_id: int, vote: Literal[-1, 0, 1], reason: Optional[VoteReason] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -526,44 +456,27 @@ class Api:
|
|||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return False
|
return False
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.post(
|
response = await session.post(
|
||||||
url=self.patch_url(f"comments/{comment_id}/vote"),
|
url=self.patch_url(f"comments/{comment_id}/vote"),
|
||||||
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
data=self.patch_auth({"vote": vote, "reason": reason.value if reason else None}),
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("success", False)
|
return obj.get("success", False)
|
||||||
|
|
||||||
@property
|
|
||||||
async def notifs(self) -> List[Notification]:
|
async def notifs(self) -> List[Notification]:
|
||||||
"""
|
"""
|
||||||
Fetches the user's notification feed.
|
Fetches the user's notification feed.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Notification]: A list of notification items.
|
List[Notification]: A list of notification items.
|
||||||
|
|
||||||
Response Structure:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"data": {
|
|
||||||
"items": [ NotificationObject, ... ],
|
|
||||||
"check_time": int, // Timestamp of the check
|
|
||||||
"username_map": [], // Deprecated or unused
|
|
||||||
"unread": {
|
|
||||||
"all": int, "upvotes": int, "mentions": int,
|
|
||||||
"comments": int, "subs": int, "total": int
|
|
||||||
},
|
|
||||||
"num_unread": int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
if not await self.ensure_login():
|
if not await self.ensure_login():
|
||||||
return []
|
return []
|
||||||
async with self as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
response = await session.get(
|
response = await session.get(
|
||||||
url=self.patch_url("users/me/notif-feed"), params=self.patch_auth()
|
url=self.patch_url("users/me/notif-feed"), params=self.patch_auth()
|
||||||
)
|
)
|
||||||
obj = await response.json()
|
obj = await response.json()
|
||||||
return obj.get("data", {}).get("items", [])
|
return obj.get("data", {}).get("items", [])
|
||||||
|
|
||||||
|
574
test.py
Normal file
574
test.py
Normal file
@ -0,0 +1,574 @@
|
|||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
BASE_URL: str = "https://devrant.com/api"
|
||||||
|
RESULTS_FILE: str = "api_test_results.json"
|
||||||
|
APP: int = 3
|
||||||
|
PLAT: int = 3
|
||||||
|
GUID: str = str(uuid.uuid4())
|
||||||
|
SID: str = str(int(datetime.now().timestamp()))
|
||||||
|
SEID: str = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Real credentials for login (using provided username/email and password)
|
||||||
|
LOGIN_USERNAME: str = "power-to@the-puff.com"
|
||||||
|
LOGIN_PASSWORD: str = "powerpuffgirl"
|
||||||
|
|
||||||
|
# Variables to store auth after login
|
||||||
|
AUTH_TOKEN_ID: Optional[str] = None
|
||||||
|
AUTH_TOKEN_KEY: Optional[str] = None
|
||||||
|
AUTH_USER_ID: Optional[str] = None
|
||||||
|
|
||||||
|
# Mock/fallback values (overridden after login or fetch)
|
||||||
|
TEST_EMAIL: str = "test@example.com"
|
||||||
|
TEST_USERNAME: str = "testuser" + str(int(datetime.now().timestamp())) # Make unique for registration
|
||||||
|
TEST_PASSWORD: str = "Test1234!"
|
||||||
|
TEST_RANT_ID: str = "1" # Will be overridden with real one
|
||||||
|
TEST_COMMENT_ID: str = "1" # Will be overridden with real one
|
||||||
|
TEST_NEWS_ID: str = "1" # Assuming this might work; adjust if needed
|
||||||
|
|
||||||
|
# Initialize results
|
||||||
|
results: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
def save_results() -> None:
|
||||||
|
"""Save the accumulated test results to JSON file."""
|
||||||
|
with open(RESULTS_FILE, 'w') as f:
|
||||||
|
json.dump(results, f, indent=2)
|
||||||
|
|
||||||
|
def test_endpoint(method: str, url: str, params: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, files: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute an API request and record the result.
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
- method: HTTP method (GET, POST, DELETE, etc.)
|
||||||
|
- url: Full API URL
|
||||||
|
- params: Query parameters (dict)
|
||||||
|
- data: POST/PUT body (dict)
|
||||||
|
- files: Multipart files (dict)
|
||||||
|
- headers: Custom headers (dict)
|
||||||
|
|
||||||
|
Response:
|
||||||
|
- Returns a dict with url, method, status_code, response (JSON or error), headers, request_body, timestamp
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.request(method, url, params=params, data=data, files=files, headers=headers)
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"url": response.url,
|
||||||
|
"method": method,
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"response": response.json() if response.content else {},
|
||||||
|
"headers": dict(response.headers),
|
||||||
|
"request_body": data or params or {},
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"url": url,
|
||||||
|
"method": method,
|
||||||
|
"status_code": None,
|
||||||
|
"response": {"error": str(e)},
|
||||||
|
"headers": {},
|
||||||
|
"request_body": data or params or {},
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
results.append(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Helper to patch auth into params/data
|
||||||
|
def patch_auth(base_dict: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Add authentication to request dict if available.
|
||||||
|
|
||||||
|
Payload: base_dict (params or data)
|
||||||
|
Response: Updated dict with app, user_id, token_id, token_key if auth present
|
||||||
|
"""
|
||||||
|
auth_dict: Dict[str, Any] = {"app": APP}
|
||||||
|
if AUTH_USER_ID and AUTH_TOKEN_ID and AUTH_TOKEN_KEY:
|
||||||
|
auth_dict.update({
|
||||||
|
"user_id": AUTH_USER_ID,
|
||||||
|
"token_id": AUTH_TOKEN_ID,
|
||||||
|
"token_key": AUTH_TOKEN_KEY
|
||||||
|
})
|
||||||
|
base_dict.update(auth_dict)
|
||||||
|
return base_dict
|
||||||
|
|
||||||
|
# Login function to get real auth tokens
|
||||||
|
def login_user() -> bool:
|
||||||
|
"""
|
||||||
|
Perform login to obtain real auth tokens.
|
||||||
|
|
||||||
|
Payload: POST to /users/auth-token with username, password, app
|
||||||
|
Response: success (bool), sets global AUTH_* variables on success
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"username": LOGIN_USERNAME,
|
||||||
|
"password": LOGIN_PASSWORD,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
result = test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params))
|
||||||
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
|
auth_token = result["response"].get("auth_token", {})
|
||||||
|
global AUTH_USER_ID, AUTH_TOKEN_ID, AUTH_TOKEN_KEY
|
||||||
|
AUTH_USER_ID = str(auth_token.get("user_id", ""))
|
||||||
|
AUTH_TOKEN_ID = str(auth_token.get("id", ""))
|
||||||
|
AUTH_TOKEN_KEY = auth_token.get("key", "")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Fetch a real rant_id from feed
|
||||||
|
def fetch_real_rant_id() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Fetch rants feed to get a real rant_id.
|
||||||
|
|
||||||
|
Payload: GET to /devrant/rants with auth
|
||||||
|
Response: First rant_id if success, else None
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
result = test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params))
|
||||||
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
|
rants = result["response"].get("rants", [])
|
||||||
|
if rants:
|
||||||
|
return str(rants[0]["id"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Post a test rant and return its id
|
||||||
|
def post_test_rant() -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Post a test rant to get a real rant_id for further tests.
|
||||||
|
|
||||||
|
Payload: POST to /devrant/rants with rant content, tags, auth
|
||||||
|
Response: rant_id if success, else None
|
||||||
|
"""
|
||||||
|
data: Dict[str, Any] = {
|
||||||
|
"rant": "Test rant for API testing (ignore)",
|
||||||
|
"tags": "test,api",
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
result = test_endpoint("POST", f"{BASE_URL}/devrant/rants", data=patch_auth(data))
|
||||||
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
|
return str(result["response"].get("rant_id", ""))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Post a test comment and return its id
|
||||||
|
def post_test_comment(rant_id: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Post a test comment to get a real comment_id.
|
||||||
|
|
||||||
|
Payload: POST to /devrant/rants/{rant_id}/comments with comment, auth
|
||||||
|
Response: comment_id if success, else None
|
||||||
|
"""
|
||||||
|
data: Dict[str, Any] = {
|
||||||
|
"comment": "Test comment for API testing (ignore)",
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
result = test_endpoint("POST", f"{BASE_URL}/devrant/rants/{rant_id}/comments", data=patch_auth(data))
|
||||||
|
if result["status_code"] == 200 and result.get("response", {}).get("success"):
|
||||||
|
return str(result["response"].get("comment_id", ""))
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Test cases with docstrings
|
||||||
|
|
||||||
|
def test_register_user() -> None:
|
||||||
|
"""
|
||||||
|
Test user registration (valid and invalid).
|
||||||
|
|
||||||
|
Payload (valid): POST /users with email, username, password, type=1, plat, guid, sid, seid, app
|
||||||
|
Expected: success=true if unique, else error
|
||||||
|
|
||||||
|
Payload (invalid): Missing email
|
||||||
|
Expected: error on email field
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"type": 1,
|
||||||
|
"email": TEST_EMAIL,
|
||||||
|
"username": TEST_USERNAME,
|
||||||
|
"password": TEST_PASSWORD,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(params.copy()))
|
||||||
|
invalid_params = params.copy()
|
||||||
|
del invalid_params["email"]
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users", data=patch_auth(invalid_params))
|
||||||
|
|
||||||
|
def test_login_user() -> None:
|
||||||
|
"""
|
||||||
|
Test user login (valid and invalid). Already done in login_user(), but record here.
|
||||||
|
|
||||||
|
Payload (valid): POST /users/auth-token with username, password, plat, guid, sid, seid, app
|
||||||
|
Expected: success=true, auth_token
|
||||||
|
|
||||||
|
Payload (invalid): Wrong password
|
||||||
|
Expected: error=Invalid login credentials
|
||||||
|
"""
|
||||||
|
# Valid is handled in login_user(); here test invalid
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"username": TEST_USERNAME,
|
||||||
|
"password": "WrongPass",
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users/auth-token", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_edit_profile() -> None:
|
||||||
|
"""
|
||||||
|
Test editing user profile.
|
||||||
|
|
||||||
|
Payload: POST /users/me/edit-profile with profile fields, plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"profile_about": "Test bio",
|
||||||
|
"profile_skills": "Python, JS",
|
||||||
|
"profile_location": "Test City",
|
||||||
|
"profile_website": "http://example.com",
|
||||||
|
"profile_github": "testuser",
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users/me/edit-profile", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_forgot_password() -> None:
|
||||||
|
"""
|
||||||
|
Test forgot password.
|
||||||
|
|
||||||
|
Payload: POST /users/forgot-password with username, plat, guid, sid, seid, app
|
||||||
|
Expected: success=true (even without auth)
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"username": LOGIN_USERNAME, # Use real one
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users/forgot-password", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_resend_confirm() -> None:
|
||||||
|
"""
|
||||||
|
Test resend confirmation email.
|
||||||
|
|
||||||
|
Payload: POST /users/me/resend-confirm with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users/me/resend-confirm", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_delete_account() -> None:
|
||||||
|
"""
|
||||||
|
Test delete account (caution: irreversible).
|
||||||
|
|
||||||
|
Payload: DELETE /users/me with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
# Comment out to avoid accidental deletion
|
||||||
|
# params: Dict[str, Any] = {
|
||||||
|
# "plat": PLAT,
|
||||||
|
# "guid": GUID,
|
||||||
|
# "sid": SID,
|
||||||
|
# "seid": SEID
|
||||||
|
# }
|
||||||
|
# test_endpoint("DELETE", f"{BASE_URL}/users/me", params=patch_auth(params))
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_mark_news_read() -> None:
|
||||||
|
"""
|
||||||
|
Test mark news as read.
|
||||||
|
|
||||||
|
Payload: POST /users/me/mark-news-read with news_id, plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"news_id": TEST_NEWS_ID,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/users/me/mark-news-read", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_get_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test get single rant.
|
||||||
|
|
||||||
|
Payload: GET /devrant/rants/{rant_id} with last_comment_id, links, plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true, rant details
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"last_comment_id": "999999999999",
|
||||||
|
"links": 0,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("GET", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_post_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test post new rant. (Already done in post_test_rant for id)
|
||||||
|
|
||||||
|
Payload: POST /devrant/rants with rant, tags, auth
|
||||||
|
Expected: success=true, rant_id
|
||||||
|
"""
|
||||||
|
# Handled in setup
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_edit_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test edit rant.
|
||||||
|
|
||||||
|
Payload: POST /devrant/rants/{rant_id} with updated rant, tags, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
data: Dict[str, Any] = {
|
||||||
|
"rant": "Updated test rant",
|
||||||
|
"tags": "test,python,update"
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", data=patch_auth(data))
|
||||||
|
|
||||||
|
def test_delete_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test delete rant.
|
||||||
|
|
||||||
|
Payload: DELETE /devrant/rants/{rant_id} with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("DELETE", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_vote_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test vote on rant (upvote and downvote with reason).
|
||||||
|
|
||||||
|
Payload: POST /devrant/rants/{rant_id}/vote with vote (1/-1), reason (optional), plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"vote": 1,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params))
|
||||||
|
params["vote"] = -1
|
||||||
|
params["reason"] = "1"
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/vote", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_favorite_rant() -> None:
|
||||||
|
"""
|
||||||
|
Test favorite/unfavorite rant.
|
||||||
|
|
||||||
|
Payload: POST /devrant/rants/{rant_id}/favorite or /unfavorite with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/favorite", data=patch_auth(params))
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/devrant/rants/{TEST_RANT_ID}/unfavorite", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_get_rant_feed() -> None:
|
||||||
|
"""
|
||||||
|
Test get rant feed.
|
||||||
|
|
||||||
|
Payload: GET /devrant/rants with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true, list of rants
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("GET", f"{BASE_URL}/devrant/rants", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_get_comment() -> None:
|
||||||
|
"""
|
||||||
|
Test get single comment.
|
||||||
|
|
||||||
|
Payload: GET /comments/{comment_id} with links, plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true, comment details
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"links": 0,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("GET", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_post_comment() -> None:
|
||||||
|
"""
|
||||||
|
Test post comment. (Handled in post_test_comment for id)
|
||||||
|
|
||||||
|
Payload: POST /devrant/rants/{rant_id}/comments with comment, auth
|
||||||
|
Expected: success=true, comment_id
|
||||||
|
"""
|
||||||
|
# Handled in setup
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_edit_comment() -> None:
|
||||||
|
"""
|
||||||
|
Test edit comment.
|
||||||
|
|
||||||
|
Payload: POST /comments/{comment_id} with updated comment, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
data: Dict[str, Any] = {
|
||||||
|
"comment": "Updated test comment"
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", data=patch_auth(data))
|
||||||
|
|
||||||
|
def test_delete_comment() -> None:
|
||||||
|
"""
|
||||||
|
Test delete comment.
|
||||||
|
|
||||||
|
Payload: DELETE /comments/{comment_id} with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("DELETE", f"{BASE_URL}/comments/{TEST_COMMENT_ID}", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_vote_comment() -> None:
|
||||||
|
"""
|
||||||
|
Test vote on comment.
|
||||||
|
|
||||||
|
Payload: POST /comments/{comment_id}/vote with vote (1), plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"vote": 1,
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("POST", f"{BASE_URL}/comments/{TEST_COMMENT_ID}/vote", data=patch_auth(params))
|
||||||
|
|
||||||
|
def test_get_notif_feed() -> None:
|
||||||
|
"""
|
||||||
|
Test get notification feed.
|
||||||
|
|
||||||
|
Payload: GET /users/me/notif-feed with ext_prof, last_time, plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true, notifications
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"ext_prof": 1,
|
||||||
|
"last_time": str(int(datetime.now().timestamp())),
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("GET", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_clear_notifications() -> None:
|
||||||
|
"""
|
||||||
|
Test clear notifications.
|
||||||
|
|
||||||
|
Payload: DELETE /users/me/notif-feed with plat, guid, sid, seid, auth
|
||||||
|
Expected: success=true
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"plat": PLAT,
|
||||||
|
"guid": GUID,
|
||||||
|
"sid": SID,
|
||||||
|
"seid": SEID
|
||||||
|
}
|
||||||
|
test_endpoint("DELETE", f"{BASE_URL}/users/me/notif-feed", params=patch_auth(params))
|
||||||
|
|
||||||
|
def test_beta_list_signup() -> None:
|
||||||
|
"""
|
||||||
|
Test beta list signup (external API).
|
||||||
|
|
||||||
|
Payload: GET https://www.hexicallabs.com/api/beta-list with email, platform, app
|
||||||
|
Expected: Whatever the API returns (may not be JSON)
|
||||||
|
"""
|
||||||
|
params: Dict[str, Any] = {
|
||||||
|
"email": TEST_EMAIL,
|
||||||
|
"platform": "test_platform"
|
||||||
|
}
|
||||||
|
test_endpoint("GET", "https://www.hexicallabs.com/api/beta-list", params=patch_auth(params))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Setup: Login and fetch real IDs
|
||||||
|
if login_user():
|
||||||
|
global TEST_RANT_ID
|
||||||
|
TEST_RANT_ID = post_test_rant() or fetch_real_rant_id() or "1"
|
||||||
|
global TEST_COMMENT_ID
|
||||||
|
TEST_COMMENT_ID = post_test_comment(TEST_RANT_ID) or "1"
|
||||||
|
|
||||||
|
test_register_user()
|
||||||
|
test_login_user()
|
||||||
|
test_edit_profile()
|
||||||
|
test_forgot_password()
|
||||||
|
test_resend_confirm()
|
||||||
|
test_mark_news_read()
|
||||||
|
test_get_rant()
|
||||||
|
test_post_rant()
|
||||||
|
test_edit_rant()
|
||||||
|
test_vote_rant()
|
||||||
|
test_favorite_rant()
|
||||||
|
test_get_rant_feed()
|
||||||
|
test_get_comment()
|
||||||
|
test_post_comment()
|
||||||
|
test_edit_comment()
|
||||||
|
test_delete_comment()
|
||||||
|
test_vote_comment()
|
||||||
|
test_get_notif_feed()
|
||||||
|
test_clear_notifications()
|
||||||
|
test_beta_list_signup()
|
||||||
|
test_delete_rant()
|
||||||
|
|
||||||
|
test_delete_account()
|
||||||
|
save_results()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in New Issue
Block a user