rxresume-python 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rxresume_python-0.2.0/rxresume_python.egg-info → rxresume_python-0.3.0}/PKG-INFO +32 -2
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/README.md +31 -1
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/pyproject.toml +1 -1
- rxresume_python-0.3.0/reactive_resume/api/__init__.py +26 -0
- rxresume_python-0.3.0/reactive_resume/api/agent.py +37 -0
- rxresume_python-0.3.0/reactive_resume/api/ai_providers.py +31 -0
- rxresume_python-0.3.0/reactive_resume/api/applications.py +78 -0
- rxresume_python-0.3.0/reactive_resume/api/statistics.py +27 -0
- rxresume_python-0.3.0/reactive_resume/api/storage.py +53 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/core/async_client.py +10 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/core/client.py +10 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/models/__init__.py +11 -0
- rxresume_python-0.3.0/reactive_resume/models/agent.py +15 -0
- rxresume_python-0.3.0/reactive_resume/models/application.py +33 -0
- rxresume_python-0.3.0/reactive_resume/models/statistics.py +14 -0
- rxresume_python-0.3.0/reactive_resume/models/storage.py +14 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0/rxresume_python.egg-info}/PKG-INFO +32 -2
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/rxresume_python.egg-info/SOURCES.txt +10 -3
- rxresume_python-0.2.0/reactive_resume/api/__init__.py +0 -11
- rxresume_python-0.2.0/tests/test_client_async.py +0 -268
- rxresume_python-0.2.0/tests/test_client_sync.py +0 -259
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/LICENSE +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/__init__.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/api/auth.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/api/resumes.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/core/__init__.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/core/exceptions.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/models/resume.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/reactive_resume/models/user.py +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/rxresume_python.egg-info/dependency_links.txt +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/rxresume_python.egg-info/requires.txt +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/rxresume_python.egg-info/top_level.txt +0 -0
- {rxresume_python-0.2.0 → rxresume_python-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rxresume-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Unofficial Python API client (SDK) for Reactive Resume v4
|
|
5
5
|
Author: Ata Can Yaymacı
|
|
6
6
|
License: MIT
|
|
@@ -42,10 +42,12 @@ It supports both synchronous (`httpx.Client`) and asynchronous (`httpx.AsyncClie
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
44
|
- **Dual client modes**: Support for both sync and async APIs.
|
|
45
|
-
- **
|
|
45
|
+
- **Full API coverage**: Integrated modules for Resume management, Auth, Job Tracker (Applications), AI Agent prompts, AI Providers configurations, Statistics, and Storage uploads.
|
|
46
|
+
- **Type safety**: Fully typed models for all entities using Pydantic V2.
|
|
46
47
|
- **Robust error handling**: Raw API status errors are automatically parsed into specific exceptions (`AuthenticationError`, `NotFoundError`, etc.).
|
|
47
48
|
- **Developer Experience (DX)**: Code-completion ready with clear typing and docstrings.
|
|
48
49
|
|
|
50
|
+
|
|
49
51
|
---
|
|
50
52
|
|
|
51
53
|
## Installation
|
|
@@ -111,8 +113,36 @@ with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as cli
|
|
|
111
113
|
print(f"Resume: {resume.name} (Slug: {resume.slug})")
|
|
112
114
|
```
|
|
113
115
|
|
|
116
|
+
### 3. Advanced Features (AI Agent, Storage, Statistics, Applications)
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as client:
|
|
120
|
+
# 1. Ask the AI agent to optimize a resume summary
|
|
121
|
+
ai_response = client.agent.chat("resume_id_here", "Suggest a professional summary for a software developer.")
|
|
122
|
+
print(f"AI Suggestion: {ai_response.response}")
|
|
123
|
+
|
|
124
|
+
# 2. Upload a profile image to storage
|
|
125
|
+
with open("avatar.png", "rb") as f:
|
|
126
|
+
file_metadata = client.storage.upload_file(f.read(), "avatar.png")
|
|
127
|
+
print(f"Uploaded Image URL: {file_metadata.url}")
|
|
128
|
+
|
|
129
|
+
# 3. Retrieve resume metrics
|
|
130
|
+
stats = client.statistics.get("resume_id_here")
|
|
131
|
+
print(f"Views: {stats.views}, Downloads: {stats.downloads}")
|
|
132
|
+
|
|
133
|
+
# 4. Log a new job application
|
|
134
|
+
from reactive_resume.models import ApplicationCreate
|
|
135
|
+
app = client.applications.create(ApplicationCreate(
|
|
136
|
+
company="Google",
|
|
137
|
+
position="Senior Backend Engineer",
|
|
138
|
+
stage="Interviewing"
|
|
139
|
+
))
|
|
140
|
+
print(f"Logged Application ID: {app.id}")
|
|
141
|
+
```
|
|
142
|
+
|
|
114
143
|
---
|
|
115
144
|
|
|
145
|
+
|
|
116
146
|
## Error Handling
|
|
117
147
|
|
|
118
148
|
All client API calls map HTTP errors to specific exception classes:
|
|
@@ -13,10 +13,12 @@ It supports both synchronous (`httpx.Client`) and asynchronous (`httpx.AsyncClie
|
|
|
13
13
|
## Features
|
|
14
14
|
|
|
15
15
|
- **Dual client modes**: Support for both sync and async APIs.
|
|
16
|
-
- **
|
|
16
|
+
- **Full API coverage**: Integrated modules for Resume management, Auth, Job Tracker (Applications), AI Agent prompts, AI Providers configurations, Statistics, and Storage uploads.
|
|
17
|
+
- **Type safety**: Fully typed models for all entities using Pydantic V2.
|
|
17
18
|
- **Robust error handling**: Raw API status errors are automatically parsed into specific exceptions (`AuthenticationError`, `NotFoundError`, etc.).
|
|
18
19
|
- **Developer Experience (DX)**: Code-completion ready with clear typing and docstrings.
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
---
|
|
21
23
|
|
|
22
24
|
## Installation
|
|
@@ -82,8 +84,36 @@ with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as cli
|
|
|
82
84
|
print(f"Resume: {resume.name} (Slug: {resume.slug})")
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
### 3. Advanced Features (AI Agent, Storage, Statistics, Applications)
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as client:
|
|
91
|
+
# 1. Ask the AI agent to optimize a resume summary
|
|
92
|
+
ai_response = client.agent.chat("resume_id_here", "Suggest a professional summary for a software developer.")
|
|
93
|
+
print(f"AI Suggestion: {ai_response.response}")
|
|
94
|
+
|
|
95
|
+
# 2. Upload a profile image to storage
|
|
96
|
+
with open("avatar.png", "rb") as f:
|
|
97
|
+
file_metadata = client.storage.upload_file(f.read(), "avatar.png")
|
|
98
|
+
print(f"Uploaded Image URL: {file_metadata.url}")
|
|
99
|
+
|
|
100
|
+
# 3. Retrieve resume metrics
|
|
101
|
+
stats = client.statistics.get("resume_id_here")
|
|
102
|
+
print(f"Views: {stats.views}, Downloads: {stats.downloads}")
|
|
103
|
+
|
|
104
|
+
# 4. Log a new job application
|
|
105
|
+
from reactive_resume.models import ApplicationCreate
|
|
106
|
+
app = client.applications.create(ApplicationCreate(
|
|
107
|
+
company="Google",
|
|
108
|
+
position="Senior Backend Engineer",
|
|
109
|
+
stage="Interviewing"
|
|
110
|
+
))
|
|
111
|
+
print(f"Logged Application ID: {app.id}")
|
|
112
|
+
```
|
|
113
|
+
|
|
85
114
|
---
|
|
86
115
|
|
|
116
|
+
|
|
87
117
|
## Error Handling
|
|
88
118
|
|
|
89
119
|
All client API calls map HTTP errors to specific exception classes:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""API endpoints and service groups."""
|
|
2
|
+
|
|
3
|
+
from .auth import AuthAPI, AsyncAuthAPI
|
|
4
|
+
from .resumes import ResumesAPI, AsyncResumesAPI
|
|
5
|
+
from .applications import ApplicationsAPI, AsyncApplicationsAPI
|
|
6
|
+
from .statistics import StatisticsAPI, AsyncStatisticsAPI
|
|
7
|
+
from .storage import StorageAPI, AsyncStorageAPI
|
|
8
|
+
from .agent import AgentAPI, AsyncAgentAPI
|
|
9
|
+
from .ai_providers import AiProvidersAPI, AsyncAiProvidersAPI
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AuthAPI",
|
|
13
|
+
"AsyncAuthAPI",
|
|
14
|
+
"ResumesAPI",
|
|
15
|
+
"AsyncResumesAPI",
|
|
16
|
+
"ApplicationsAPI",
|
|
17
|
+
"AsyncApplicationsAPI",
|
|
18
|
+
"StatisticsAPI",
|
|
19
|
+
"AsyncStatisticsAPI",
|
|
20
|
+
"StorageAPI",
|
|
21
|
+
"AsyncStorageAPI",
|
|
22
|
+
"AgentAPI",
|
|
23
|
+
"AsyncAgentAPI",
|
|
24
|
+
"AiProvidersAPI",
|
|
25
|
+
"AsyncAiProvidersAPI",
|
|
26
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Agent API endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from ..models.agent import AgentRequest, AgentResponse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentAPI:
|
|
7
|
+
"""Synchronous AI Agent operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def chat(self, resume_id: str, prompt: str) -> AgentResponse:
|
|
13
|
+
"""Send a prompt to the AI Agent relative to a specific resume context."""
|
|
14
|
+
payload = AgentRequest(prompt=prompt).model_dump()
|
|
15
|
+
response = self._client._request(
|
|
16
|
+
"POST",
|
|
17
|
+
f"/api/openapi/agent/{resume_id}/chat",
|
|
18
|
+
json=payload,
|
|
19
|
+
)
|
|
20
|
+
return AgentResponse.model_validate(response)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncAgentAPI:
|
|
24
|
+
"""Asynchronous AI Agent operations."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, client) -> None:
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
async def chat(self, resume_id: str, prompt: str) -> AgentResponse:
|
|
30
|
+
"""Send a prompt to the AI Agent asynchronously relative to a specific resume context."""
|
|
31
|
+
payload = AgentRequest(prompt=prompt).model_dump()
|
|
32
|
+
response = await self._client._request(
|
|
33
|
+
"POST",
|
|
34
|
+
f"/api/openapi/agent/{resume_id}/chat",
|
|
35
|
+
json=payload,
|
|
36
|
+
)
|
|
37
|
+
return AgentResponse.model_validate(response)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""AI Providers API endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AiProvidersAPI:
|
|
7
|
+
"""Synchronous AI Providers operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self) -> List[Dict[str, Any]]:
|
|
13
|
+
"""List all configured AI Providers."""
|
|
14
|
+
response = self._client._request("GET", "/api/openapi/ai-providers")
|
|
15
|
+
if isinstance(response, list):
|
|
16
|
+
return response
|
|
17
|
+
return [response]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AsyncAiProvidersAPI:
|
|
21
|
+
"""Asynchronous AI Providers operations."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, client) -> None:
|
|
24
|
+
self._client = client
|
|
25
|
+
|
|
26
|
+
async def list(self) -> List[Dict[str, Any]]:
|
|
27
|
+
"""List all configured AI Providers asynchronously."""
|
|
28
|
+
response = await self._client._request("GET", "/api/openapi/ai-providers")
|
|
29
|
+
if isinstance(response, list):
|
|
30
|
+
return response
|
|
31
|
+
return [response]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Applications API endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from ..models.application import Application, ApplicationCreate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ApplicationsAPI:
|
|
8
|
+
"""Synchronous Applications operations."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client) -> None:
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
def list(self) -> List[Application]:
|
|
14
|
+
"""List all job applications."""
|
|
15
|
+
response = self._client._request("GET", "/api/openapi/applications")
|
|
16
|
+
if isinstance(response, dict) and "data" in response:
|
|
17
|
+
items = response["data"]
|
|
18
|
+
else:
|
|
19
|
+
items = response
|
|
20
|
+
return [Application.model_validate(item) for item in items]
|
|
21
|
+
|
|
22
|
+
def get(self, app_id: str) -> Application:
|
|
23
|
+
"""Retrieve a specific job application by ID."""
|
|
24
|
+
response = self._client._request("GET", f"/api/openapi/applications/{app_id}")
|
|
25
|
+
return Application.model_validate(response)
|
|
26
|
+
|
|
27
|
+
def create(self, data: ApplicationCreate) -> Application:
|
|
28
|
+
"""Log/create a new job application."""
|
|
29
|
+
payload = data.model_dump(by_alias=True, exclude_none=True)
|
|
30
|
+
response = self._client._request("POST", "/api/openapi/applications", json=payload)
|
|
31
|
+
return Application.model_validate(response)
|
|
32
|
+
|
|
33
|
+
def update(self, app_id: str, data: Dict[str, Any]) -> Application:
|
|
34
|
+
"""Update an existing job application."""
|
|
35
|
+
response = self._client._request("PATCH", f"/api/openapi/applications/{app_id}", json=data)
|
|
36
|
+
return Application.model_validate(response)
|
|
37
|
+
|
|
38
|
+
def delete(self, app_id: str) -> None:
|
|
39
|
+
"""Delete a job application by ID."""
|
|
40
|
+
self._client._request("DELETE", f"/api/openapi/applications/{app_id}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AsyncApplicationsAPI:
|
|
44
|
+
"""Asynchronous Applications operations."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, client) -> None:
|
|
47
|
+
self._client = client
|
|
48
|
+
|
|
49
|
+
async def list(self) -> List[Application]:
|
|
50
|
+
"""List all job applications asynchronously."""
|
|
51
|
+
response = await self._client._request("GET", "/api/openapi/applications")
|
|
52
|
+
if isinstance(response, dict) and "data" in response:
|
|
53
|
+
items = response["data"]
|
|
54
|
+
else:
|
|
55
|
+
items = response
|
|
56
|
+
return [Application.model_validate(item) for item in items]
|
|
57
|
+
|
|
58
|
+
async def get(self, app_id: str) -> Application:
|
|
59
|
+
"""Retrieve a specific job application by ID asynchronously."""
|
|
60
|
+
response = await self._client._request("GET", f"/api/openapi/applications/{app_id}")
|
|
61
|
+
return Application.model_validate(response)
|
|
62
|
+
|
|
63
|
+
async def create(self, data: ApplicationCreate) -> Application:
|
|
64
|
+
"""Log/create a new job application asynchronously."""
|
|
65
|
+
payload = data.model_dump(by_alias=True, exclude_none=True)
|
|
66
|
+
response = await self._client._request("POST", "/api/openapi/applications", json=payload)
|
|
67
|
+
return Application.model_validate(response)
|
|
68
|
+
|
|
69
|
+
async def update(self, app_id: str, data: Dict[str, Any]) -> Application:
|
|
70
|
+
"""Update an existing job application asynchronously."""
|
|
71
|
+
response = await self._client._request(
|
|
72
|
+
"PATCH", f"/api/openapi/applications/{app_id}", json=data
|
|
73
|
+
)
|
|
74
|
+
return Application.model_validate(response)
|
|
75
|
+
|
|
76
|
+
async def delete(self, app_id: str) -> None:
|
|
77
|
+
"""Delete a job application by ID asynchronously."""
|
|
78
|
+
await self._client._request("DELETE", f"/api/openapi/applications/{app_id}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Statistics API endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from ..models.statistics import ResumeStats
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StatisticsAPI:
|
|
7
|
+
"""Synchronous Statistics operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def get(self, resume_id: str) -> ResumeStats:
|
|
13
|
+
"""Retrieve interaction statistics for a specific resume."""
|
|
14
|
+
response = self._client._request("GET", f"/api/openapi/statistics/{resume_id}")
|
|
15
|
+
return ResumeStats.model_validate(response)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsyncStatisticsAPI:
|
|
19
|
+
"""Asynchronous Statistics operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, client) -> None:
|
|
22
|
+
self._client = client
|
|
23
|
+
|
|
24
|
+
async def get(self, resume_id: str) -> ResumeStats:
|
|
25
|
+
"""Retrieve interaction statistics for a specific resume asynchronously."""
|
|
26
|
+
response = await self._client._request("GET", f"/api/openapi/statistics/{resume_id}")
|
|
27
|
+
return ResumeStats.model_validate(response)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Storage API endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from ..models.storage import StorageFile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StorageAPI:
|
|
7
|
+
"""Synchronous Storage operations."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def upload_file(self, file_content: bytes, filename: str) -> StorageFile:
|
|
13
|
+
"""Upload a file to storage.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
file_content: Raw bytes of the file.
|
|
17
|
+
filename: Target name for the uploaded file.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Metadata details of the uploaded file.
|
|
21
|
+
"""
|
|
22
|
+
files = {"file": (filename, file_content)}
|
|
23
|
+
response = self._client._request(
|
|
24
|
+
"POST",
|
|
25
|
+
"/api/openapi/storage/upload",
|
|
26
|
+
files=files,
|
|
27
|
+
)
|
|
28
|
+
return StorageFile.model_validate(response)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AsyncStorageAPI:
|
|
32
|
+
"""Asynchronous Storage operations."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, client) -> None:
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
async def upload_file(self, file_content: bytes, filename: str) -> StorageFile:
|
|
38
|
+
"""Upload a file to storage asynchronously.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
file_content: Raw bytes of the file.
|
|
42
|
+
filename: Target name for the uploaded file.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Metadata details of the uploaded file.
|
|
46
|
+
"""
|
|
47
|
+
files = {"file": (filename, file_content)}
|
|
48
|
+
response = await self._client._request(
|
|
49
|
+
"POST",
|
|
50
|
+
"/api/openapi/storage/upload",
|
|
51
|
+
files=files,
|
|
52
|
+
)
|
|
53
|
+
return StorageFile.model_validate(response)
|
|
@@ -11,6 +11,11 @@ from .exceptions import (
|
|
|
11
11
|
)
|
|
12
12
|
from ..api.auth import AsyncAuthAPI
|
|
13
13
|
from ..api.resumes import AsyncResumesAPI
|
|
14
|
+
from ..api.applications import AsyncApplicationsAPI
|
|
15
|
+
from ..api.statistics import AsyncStatisticsAPI
|
|
16
|
+
from ..api.storage import AsyncStorageAPI
|
|
17
|
+
from ..api.agent import AsyncAgentAPI
|
|
18
|
+
from ..api.ai_providers import AsyncAiProvidersAPI
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
class AsyncRxResumeClient:
|
|
@@ -57,6 +62,11 @@ class AsyncRxResumeClient:
|
|
|
57
62
|
# Initialize API sections
|
|
58
63
|
self.auth = AsyncAuthAPI(self)
|
|
59
64
|
self.resumes = AsyncResumesAPI(self)
|
|
65
|
+
self.applications = AsyncApplicationsAPI(self)
|
|
66
|
+
self.statistics = AsyncStatisticsAPI(self)
|
|
67
|
+
self.storage = AsyncStorageAPI(self)
|
|
68
|
+
self.agent = AsyncAgentAPI(self)
|
|
69
|
+
self.ai_providers = AsyncAiProvidersAPI(self)
|
|
60
70
|
|
|
61
71
|
def set_token(self, token: str) -> None:
|
|
62
72
|
"""Update client headers with a new Bearer token."""
|
|
@@ -11,6 +11,11 @@ from .exceptions import (
|
|
|
11
11
|
)
|
|
12
12
|
from ..api.auth import AuthAPI
|
|
13
13
|
from ..api.resumes import ResumesAPI
|
|
14
|
+
from ..api.applications import ApplicationsAPI
|
|
15
|
+
from ..api.statistics import StatisticsAPI
|
|
16
|
+
from ..api.storage import StorageAPI
|
|
17
|
+
from ..api.agent import AgentAPI
|
|
18
|
+
from ..api.ai_providers import AiProvidersAPI
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
class RxResumeClient:
|
|
@@ -57,6 +62,11 @@ class RxResumeClient:
|
|
|
57
62
|
# Initialize API sections
|
|
58
63
|
self.auth = AuthAPI(self)
|
|
59
64
|
self.resumes = ResumesAPI(self)
|
|
65
|
+
self.applications = ApplicationsAPI(self)
|
|
66
|
+
self.statistics = StatisticsAPI(self)
|
|
67
|
+
self.storage = StorageAPI(self)
|
|
68
|
+
self.agent = AgentAPI(self)
|
|
69
|
+
self.ai_providers = AiProvidersAPI(self)
|
|
60
70
|
|
|
61
71
|
def set_token(self, token: str) -> None:
|
|
62
72
|
"""Update client headers with a new Bearer token."""
|
|
@@ -24,6 +24,11 @@ from .resume import (
|
|
|
24
24
|
ResumeImportData,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
+
from .application import Application, ApplicationCreate
|
|
28
|
+
from .statistics import ResumeStats
|
|
29
|
+
from .storage import StorageFile
|
|
30
|
+
from .agent import AgentRequest, AgentResponse
|
|
31
|
+
|
|
27
32
|
__all__ = [
|
|
28
33
|
"User",
|
|
29
34
|
"URLModel",
|
|
@@ -46,4 +51,10 @@ __all__ = [
|
|
|
46
51
|
"ResumeData",
|
|
47
52
|
"Resume",
|
|
48
53
|
"ResumeImportData",
|
|
54
|
+
"Application",
|
|
55
|
+
"ApplicationCreate",
|
|
56
|
+
"ResumeStats",
|
|
57
|
+
"StorageFile",
|
|
58
|
+
"AgentRequest",
|
|
59
|
+
"AgentResponse",
|
|
49
60
|
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Pydantic models representing AI Agent communication in Reactive Resume."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AgentRequest(BaseModel):
|
|
7
|
+
"""Payload to send to the AI Agent."""
|
|
8
|
+
|
|
9
|
+
prompt: str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentResponse(BaseModel):
|
|
13
|
+
"""Response returned by the AI Agent."""
|
|
14
|
+
|
|
15
|
+
response: str
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Pydantic models representing job applications in Reactive Resume."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Application(BaseModel):
|
|
9
|
+
"""Represents a job application in the tracker."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
user_id: str = Field(..., alias="userId")
|
|
15
|
+
company: str
|
|
16
|
+
position: str
|
|
17
|
+
stage: str = "Applied" # Applied, Interviewing, Offered, Rejected
|
|
18
|
+
date: datetime
|
|
19
|
+
summary: str = ""
|
|
20
|
+
url: str = ""
|
|
21
|
+
created_at: datetime = Field(..., alias="createdAt")
|
|
22
|
+
updated_at: datetime = Field(..., alias="updatedAt")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ApplicationCreate(BaseModel):
|
|
26
|
+
"""Payload to log/create a new job application."""
|
|
27
|
+
|
|
28
|
+
company: str
|
|
29
|
+
position: str
|
|
30
|
+
stage: Optional[str] = "Applied"
|
|
31
|
+
date: Optional[datetime] = None
|
|
32
|
+
summary: Optional[str] = ""
|
|
33
|
+
url: Optional[str] = ""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic models representing resume interaction statistics."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResumeStats(BaseModel):
|
|
8
|
+
"""Represents view and download statistics for a resume."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
11
|
+
|
|
12
|
+
views: int = 0
|
|
13
|
+
downloads: int = 0
|
|
14
|
+
history: Dict[str, Any] = {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Pydantic models representing storage files in Reactive Resume."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StorageFile(BaseModel):
|
|
7
|
+
"""Represents a file/blob uploaded to Reactive Resume storage."""
|
|
8
|
+
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
filename: str
|
|
12
|
+
url: str
|
|
13
|
+
size: int
|
|
14
|
+
mime_type: str = Field(..., alias="mimeType")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rxresume-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Unofficial Python API client (SDK) for Reactive Resume v4
|
|
5
5
|
Author: Ata Can Yaymacı
|
|
6
6
|
License: MIT
|
|
@@ -42,10 +42,12 @@ It supports both synchronous (`httpx.Client`) and asynchronous (`httpx.AsyncClie
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
44
|
- **Dual client modes**: Support for both sync and async APIs.
|
|
45
|
-
- **
|
|
45
|
+
- **Full API coverage**: Integrated modules for Resume management, Auth, Job Tracker (Applications), AI Agent prompts, AI Providers configurations, Statistics, and Storage uploads.
|
|
46
|
+
- **Type safety**: Fully typed models for all entities using Pydantic V2.
|
|
46
47
|
- **Robust error handling**: Raw API status errors are automatically parsed into specific exceptions (`AuthenticationError`, `NotFoundError`, etc.).
|
|
47
48
|
- **Developer Experience (DX)**: Code-completion ready with clear typing and docstrings.
|
|
48
49
|
|
|
50
|
+
|
|
49
51
|
---
|
|
50
52
|
|
|
51
53
|
## Installation
|
|
@@ -111,8 +113,36 @@ with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as cli
|
|
|
111
113
|
print(f"Resume: {resume.name} (Slug: {resume.slug})")
|
|
112
114
|
```
|
|
113
115
|
|
|
116
|
+
### 3. Advanced Features (AI Agent, Storage, Statistics, Applications)
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as client:
|
|
120
|
+
# 1. Ask the AI agent to optimize a resume summary
|
|
121
|
+
ai_response = client.agent.chat("resume_id_here", "Suggest a professional summary for a software developer.")
|
|
122
|
+
print(f"AI Suggestion: {ai_response.response}")
|
|
123
|
+
|
|
124
|
+
# 2. Upload a profile image to storage
|
|
125
|
+
with open("avatar.png", "rb") as f:
|
|
126
|
+
file_metadata = client.storage.upload_file(f.read(), "avatar.png")
|
|
127
|
+
print(f"Uploaded Image URL: {file_metadata.url}")
|
|
128
|
+
|
|
129
|
+
# 3. Retrieve resume metrics
|
|
130
|
+
stats = client.statistics.get("resume_id_here")
|
|
131
|
+
print(f"Views: {stats.views}, Downloads: {stats.downloads}")
|
|
132
|
+
|
|
133
|
+
# 4. Log a new job application
|
|
134
|
+
from reactive_resume.models import ApplicationCreate
|
|
135
|
+
app = client.applications.create(ApplicationCreate(
|
|
136
|
+
company="Google",
|
|
137
|
+
position="Senior Backend Engineer",
|
|
138
|
+
stage="Interviewing"
|
|
139
|
+
))
|
|
140
|
+
print(f"Logged Application ID: {app.id}")
|
|
141
|
+
```
|
|
142
|
+
|
|
114
143
|
---
|
|
115
144
|
|
|
145
|
+
|
|
116
146
|
## Error Handling
|
|
117
147
|
|
|
118
148
|
All client API calls map HTTP errors to specific exception classes:
|
|
@@ -3,19 +3,26 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
reactive_resume/__init__.py
|
|
5
5
|
reactive_resume/api/__init__.py
|
|
6
|
+
reactive_resume/api/agent.py
|
|
7
|
+
reactive_resume/api/ai_providers.py
|
|
8
|
+
reactive_resume/api/applications.py
|
|
6
9
|
reactive_resume/api/auth.py
|
|
7
10
|
reactive_resume/api/resumes.py
|
|
11
|
+
reactive_resume/api/statistics.py
|
|
12
|
+
reactive_resume/api/storage.py
|
|
8
13
|
reactive_resume/core/__init__.py
|
|
9
14
|
reactive_resume/core/async_client.py
|
|
10
15
|
reactive_resume/core/client.py
|
|
11
16
|
reactive_resume/core/exceptions.py
|
|
12
17
|
reactive_resume/models/__init__.py
|
|
18
|
+
reactive_resume/models/agent.py
|
|
19
|
+
reactive_resume/models/application.py
|
|
13
20
|
reactive_resume/models/resume.py
|
|
21
|
+
reactive_resume/models/statistics.py
|
|
22
|
+
reactive_resume/models/storage.py
|
|
14
23
|
reactive_resume/models/user.py
|
|
15
24
|
rxresume_python.egg-info/PKG-INFO
|
|
16
25
|
rxresume_python.egg-info/SOURCES.txt
|
|
17
26
|
rxresume_python.egg-info/dependency_links.txt
|
|
18
27
|
rxresume_python.egg-info/requires.txt
|
|
19
|
-
rxresume_python.egg-info/top_level.txt
|
|
20
|
-
tests/test_client_async.py
|
|
21
|
-
tests/test_client_sync.py
|
|
28
|
+
rxresume_python.egg-info/top_level.txt
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import respx
|
|
3
|
-
import httpx
|
|
4
|
-
from httpx import Response
|
|
5
|
-
from reactive_resume import (
|
|
6
|
-
AsyncRxResumeClient,
|
|
7
|
-
AuthenticationError,
|
|
8
|
-
NotFoundError,
|
|
9
|
-
ReactiveResumeAPIError,
|
|
10
|
-
ReactiveResumeError,
|
|
11
|
-
)
|
|
12
|
-
from reactive_resume.models import ResumeImportData, Resume, User
|
|
13
|
-
|
|
14
|
-
BASE_URL = "https://rxresu.me"
|
|
15
|
-
API_KEY = "test-api-key"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
async def async_client():
|
|
20
|
-
async with AsyncRxResumeClient(base_url=BASE_URL, api_key=API_KEY) as c:
|
|
21
|
-
yield c
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@pytest.mark.asyncio
|
|
25
|
-
@respx.mock
|
|
26
|
-
async def test_list_resumes_async(async_client):
|
|
27
|
-
mock_resume = {
|
|
28
|
-
"id": "test-resume-id",
|
|
29
|
-
"name": "Test CV",
|
|
30
|
-
"slug": "test-cv",
|
|
31
|
-
"userId": "user-123",
|
|
32
|
-
"visibility": "public",
|
|
33
|
-
"locked": False,
|
|
34
|
-
"data": {
|
|
35
|
-
"basics": {
|
|
36
|
-
"name": "John Doe",
|
|
37
|
-
"headline": "Software Developer",
|
|
38
|
-
"email": "john@example.com",
|
|
39
|
-
"phone": "123456",
|
|
40
|
-
"website": "https://john.me",
|
|
41
|
-
"location": "Earth",
|
|
42
|
-
"picture": "",
|
|
43
|
-
"profiles": [],
|
|
44
|
-
},
|
|
45
|
-
"sections": {},
|
|
46
|
-
},
|
|
47
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
48
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
52
|
-
return_value=Response(200, json=[mock_resume])
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
resumes = await async_client.resumes.list()
|
|
56
|
-
assert len(resumes) == 1
|
|
57
|
-
assert isinstance(resumes[0], Resume)
|
|
58
|
-
assert resumes[0].id == "test-resume-id"
|
|
59
|
-
assert resumes[0].data.basics.name == "John Doe"
|
|
60
|
-
assert route.called
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@pytest.mark.asyncio
|
|
64
|
-
@respx.mock
|
|
65
|
-
async def test_get_resume_async(async_client):
|
|
66
|
-
mock_resume = {
|
|
67
|
-
"id": "resume-1",
|
|
68
|
-
"name": "Test CV 1",
|
|
69
|
-
"slug": "test-cv-1",
|
|
70
|
-
"userId": "user-123",
|
|
71
|
-
"data": {"basics": {}, "sections": {}},
|
|
72
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
73
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
74
|
-
}
|
|
75
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resume/resume-1").mock(
|
|
76
|
-
return_value=Response(200, json=mock_resume)
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
resume = await async_client.resumes.get("resume-1")
|
|
80
|
-
assert resume.id == "resume-1"
|
|
81
|
-
assert route.called
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.mark.asyncio
|
|
85
|
-
@respx.mock
|
|
86
|
-
async def test_create_resume_async(async_client):
|
|
87
|
-
mock_resume = {
|
|
88
|
-
"id": "new-resume",
|
|
89
|
-
"name": "My New CV",
|
|
90
|
-
"slug": "my-new-cv",
|
|
91
|
-
"userId": "user-123",
|
|
92
|
-
"data": {"basics": {}, "sections": {}},
|
|
93
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
94
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
95
|
-
}
|
|
96
|
-
route = respx.post(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
97
|
-
return_value=Response(201, json=mock_resume)
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
import_data = ResumeImportData(title="My New CV")
|
|
101
|
-
resume = await async_client.resumes.import_resume(import_data)
|
|
102
|
-
assert resume.id == "new-resume"
|
|
103
|
-
assert route.called
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@pytest.mark.asyncio
|
|
107
|
-
@respx.mock
|
|
108
|
-
async def test_update_resume_async(async_client):
|
|
109
|
-
mock_resume = {
|
|
110
|
-
"id": "resume-1",
|
|
111
|
-
"name": "Updated CV Name",
|
|
112
|
-
"slug": "updated-cv-name",
|
|
113
|
-
"userId": "user-123",
|
|
114
|
-
"data": {"basics": {}, "sections": {}},
|
|
115
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
116
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
117
|
-
}
|
|
118
|
-
route = respx.patch(f"{BASE_URL}/api/openapi/resume/resume-1").mock(
|
|
119
|
-
return_value=Response(200, json=mock_resume)
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
resume = await async_client.resumes.update("resume-1", {"name": "Updated CV Name"})
|
|
123
|
-
assert resume.name == "Updated CV Name"
|
|
124
|
-
assert route.called
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@pytest.mark.asyncio
|
|
128
|
-
@respx.mock
|
|
129
|
-
async def test_delete_resume_async(async_client):
|
|
130
|
-
route = respx.delete(f"{BASE_URL}/api/openapi/resume/resume-1").mock(return_value=Response(204))
|
|
131
|
-
await async_client.resumes.delete("resume-1")
|
|
132
|
-
assert route.called
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@pytest.mark.asyncio
|
|
136
|
-
async def test_get_pdf_url_async(async_client):
|
|
137
|
-
pdf_url = await async_client.resumes.get_pdf_url("resume-1")
|
|
138
|
-
assert pdf_url == f"{BASE_URL}/api/openapi/resume/resume-1/pdf"
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@pytest.mark.asyncio
|
|
142
|
-
@respx.mock
|
|
143
|
-
async def test_auth_login_async(async_client):
|
|
144
|
-
mock_user = {
|
|
145
|
-
"id": "user-123",
|
|
146
|
-
"name": "Ata Can",
|
|
147
|
-
"username": "atacan",
|
|
148
|
-
"email": "ata@example.com",
|
|
149
|
-
"provider": "email",
|
|
150
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
151
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
152
|
-
}
|
|
153
|
-
route = respx.post(f"{BASE_URL}/api/auth/login").mock(
|
|
154
|
-
return_value=Response(200, json={"token": "fake-jwt-token", "user": mock_user})
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
token, user = await async_client.auth.login("ata@example.com", "password123")
|
|
158
|
-
assert token == "fake-jwt-token"
|
|
159
|
-
assert isinstance(user, User)
|
|
160
|
-
assert user.username == "atacan"
|
|
161
|
-
assert route.called
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
@pytest.mark.asyncio
|
|
165
|
-
@respx.mock
|
|
166
|
-
async def test_auth_me_async(async_client):
|
|
167
|
-
mock_user = {
|
|
168
|
-
"id": "user-123",
|
|
169
|
-
"name": "Ata Can",
|
|
170
|
-
"username": "atacan",
|
|
171
|
-
"email": "ata@example.com",
|
|
172
|
-
"provider": "email",
|
|
173
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
174
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
175
|
-
}
|
|
176
|
-
route = respx.get(f"{BASE_URL}/api/user/me").mock(return_value=Response(200, json=mock_user))
|
|
177
|
-
|
|
178
|
-
user = await async_client.auth.me()
|
|
179
|
-
assert isinstance(user, User)
|
|
180
|
-
assert user.id == "user-123"
|
|
181
|
-
assert route.called
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@pytest.mark.asyncio
|
|
185
|
-
@respx.mock
|
|
186
|
-
async def test_authentication_error_async(async_client):
|
|
187
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
188
|
-
return_value=Response(401, json={"message": "Invalid API Key"})
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
with pytest.raises(AuthenticationError):
|
|
192
|
-
await async_client.resumes.list()
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
@pytest.mark.asyncio
|
|
196
|
-
@respx.mock
|
|
197
|
-
async def test_not_found_error_async(async_client):
|
|
198
|
-
respx.get(f"{BASE_URL}/api/openapi/resume/non-existent").mock(
|
|
199
|
-
return_value=Response(404, json={"message": "Not found"})
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
with pytest.raises(NotFoundError):
|
|
203
|
-
await async_client.resumes.get("non-existent")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@pytest.mark.asyncio
|
|
207
|
-
@respx.mock
|
|
208
|
-
async def test_generic_api_error_500_async(async_client):
|
|
209
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
210
|
-
return_value=Response(500, text="Internal Server Error")
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
with pytest.raises(ReactiveResumeAPIError) as exc_info:
|
|
214
|
-
await async_client.resumes.list()
|
|
215
|
-
assert exc_info.value.status_code == 500
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
@pytest.mark.asyncio
|
|
219
|
-
@respx.mock
|
|
220
|
-
async def test_network_error_async(async_client):
|
|
221
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
222
|
-
side_effect=httpx.ConnectTimeout("Connection timed out")
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
with pytest.raises(ReactiveResumeError) as exc_info:
|
|
226
|
-
await async_client.resumes.list()
|
|
227
|
-
assert "Network or connection error occurred" in str(exc_info.value)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
@pytest.mark.asyncio
|
|
231
|
-
async def test_token_api_key_modifiers_async(async_client):
|
|
232
|
-
async_client.set_token("new-token")
|
|
233
|
-
assert async_client.client.headers["Authorization"] == "Bearer new-token"
|
|
234
|
-
assert "x-api-key" not in async_client.client.headers
|
|
235
|
-
|
|
236
|
-
async_client.set_api_key("new-api-key")
|
|
237
|
-
assert async_client.client.headers["x-api-key"] == "new-api-key"
|
|
238
|
-
assert "Authorization" not in async_client.client.headers
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@pytest.mark.asyncio
|
|
242
|
-
async def test_token_initialization_async():
|
|
243
|
-
async with AsyncRxResumeClient(base_url=BASE_URL, token="initial-token") as c:
|
|
244
|
-
assert c.client.headers["Authorization"] == "Bearer initial-token"
|
|
245
|
-
assert "x-api-key" not in c.client.headers
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
@pytest.mark.asyncio
|
|
249
|
-
@respx.mock
|
|
250
|
-
async def test_list_resumes_pagination_async(async_client):
|
|
251
|
-
mock_resume = {
|
|
252
|
-
"id": "paginated-resume-id",
|
|
253
|
-
"name": "Paginated CV",
|
|
254
|
-
"slug": "paginated-cv",
|
|
255
|
-
"userId": "user-123",
|
|
256
|
-
"visibility": "public",
|
|
257
|
-
"locked": False,
|
|
258
|
-
"data": {"basics": {}, "sections": {}},
|
|
259
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
260
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
261
|
-
}
|
|
262
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
263
|
-
return_value=Response(200, json={"data": [mock_resume]})
|
|
264
|
-
)
|
|
265
|
-
resumes = await async_client.resumes.list()
|
|
266
|
-
assert len(resumes) == 1
|
|
267
|
-
assert resumes[0].id == "paginated-resume-id"
|
|
268
|
-
assert route.called
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
import respx
|
|
3
|
-
import httpx
|
|
4
|
-
from httpx import Response
|
|
5
|
-
from reactive_resume import (
|
|
6
|
-
RxResumeClient,
|
|
7
|
-
AuthenticationError,
|
|
8
|
-
NotFoundError,
|
|
9
|
-
ReactiveResumeAPIError,
|
|
10
|
-
ReactiveResumeError,
|
|
11
|
-
)
|
|
12
|
-
from reactive_resume.models import ResumeImportData, Resume, User
|
|
13
|
-
|
|
14
|
-
BASE_URL = "https://rxresu.me"
|
|
15
|
-
API_KEY = "test-api-key"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def client():
|
|
20
|
-
with RxResumeClient(base_url=BASE_URL, api_key=API_KEY) as c:
|
|
21
|
-
yield c
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@respx.mock
|
|
25
|
-
def test_list_resumes(client):
|
|
26
|
-
mock_resume = {
|
|
27
|
-
"id": "test-resume-id",
|
|
28
|
-
"name": "Test CV",
|
|
29
|
-
"slug": "test-cv",
|
|
30
|
-
"userId": "user-123",
|
|
31
|
-
"visibility": "public",
|
|
32
|
-
"locked": False,
|
|
33
|
-
"data": {
|
|
34
|
-
"basics": {
|
|
35
|
-
"name": "John Doe",
|
|
36
|
-
"headline": "Software Developer",
|
|
37
|
-
"email": "john@example.com",
|
|
38
|
-
"phone": "123456",
|
|
39
|
-
"website": "https://john.me",
|
|
40
|
-
"location": "Earth",
|
|
41
|
-
"picture": "",
|
|
42
|
-
"profiles": [],
|
|
43
|
-
},
|
|
44
|
-
"sections": {},
|
|
45
|
-
},
|
|
46
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
47
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
51
|
-
return_value=Response(200, json=[mock_resume])
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
resumes = client.resumes.list()
|
|
55
|
-
assert len(resumes) == 1
|
|
56
|
-
assert isinstance(resumes[0], Resume)
|
|
57
|
-
assert resumes[0].id == "test-resume-id"
|
|
58
|
-
assert resumes[0].data.basics.name == "John Doe"
|
|
59
|
-
assert route.called
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@respx.mock
|
|
63
|
-
def test_get_resume(client):
|
|
64
|
-
mock_resume = {
|
|
65
|
-
"id": "resume-1",
|
|
66
|
-
"name": "Test CV 1",
|
|
67
|
-
"slug": "test-cv-1",
|
|
68
|
-
"userId": "user-123",
|
|
69
|
-
"data": {"basics": {}, "sections": {}},
|
|
70
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
71
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
72
|
-
}
|
|
73
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resume/resume-1").mock(
|
|
74
|
-
return_value=Response(200, json=mock_resume)
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
resume = client.resumes.get("resume-1")
|
|
78
|
-
assert resume.id == "resume-1"
|
|
79
|
-
assert route.called
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@respx.mock
|
|
83
|
-
def test_create_resume(client):
|
|
84
|
-
mock_resume = {
|
|
85
|
-
"id": "new-resume",
|
|
86
|
-
"name": "My New CV",
|
|
87
|
-
"slug": "my-new-cv",
|
|
88
|
-
"userId": "user-123",
|
|
89
|
-
"data": {"basics": {}, "sections": {}},
|
|
90
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
91
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
92
|
-
}
|
|
93
|
-
route = respx.post(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
94
|
-
return_value=Response(201, json=mock_resume)
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
import_data = ResumeImportData(title="My New CV")
|
|
98
|
-
resume = client.resumes.import_resume(import_data)
|
|
99
|
-
assert resume.id == "new-resume"
|
|
100
|
-
assert route.called
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
@respx.mock
|
|
104
|
-
def test_update_resume(client):
|
|
105
|
-
mock_resume = {
|
|
106
|
-
"id": "resume-1",
|
|
107
|
-
"name": "Updated CV Name",
|
|
108
|
-
"slug": "updated-cv-name",
|
|
109
|
-
"userId": "user-123",
|
|
110
|
-
"data": {"basics": {}, "sections": {}},
|
|
111
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
112
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
113
|
-
}
|
|
114
|
-
route = respx.patch(f"{BASE_URL}/api/openapi/resume/resume-1").mock(
|
|
115
|
-
return_value=Response(200, json=mock_resume)
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
resume = client.resumes.update("resume-1", {"name": "Updated CV Name"})
|
|
119
|
-
assert resume.name == "Updated CV Name"
|
|
120
|
-
assert route.called
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@respx.mock
|
|
124
|
-
def test_delete_resume(client):
|
|
125
|
-
route = respx.delete(f"{BASE_URL}/api/openapi/resume/resume-1").mock(return_value=Response(204))
|
|
126
|
-
client.resumes.delete("resume-1")
|
|
127
|
-
assert route.called
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def test_get_pdf_url(client):
|
|
131
|
-
pdf_url = client.resumes.get_pdf_url("resume-1")
|
|
132
|
-
assert pdf_url == f"{BASE_URL}/api/openapi/resume/resume-1/pdf"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@respx.mock
|
|
136
|
-
def test_auth_login(client):
|
|
137
|
-
mock_user = {
|
|
138
|
-
"id": "user-123",
|
|
139
|
-
"name": "Ata Can",
|
|
140
|
-
"username": "atacan",
|
|
141
|
-
"email": "ata@example.com",
|
|
142
|
-
"provider": "email",
|
|
143
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
144
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
145
|
-
}
|
|
146
|
-
route = respx.post(f"{BASE_URL}/api/auth/login").mock(
|
|
147
|
-
return_value=Response(200, json={"token": "fake-jwt-token", "user": mock_user})
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
token, user = client.auth.login("ata@example.com", "password123")
|
|
151
|
-
assert token == "fake-jwt-token"
|
|
152
|
-
assert isinstance(user, User)
|
|
153
|
-
assert user.username == "atacan"
|
|
154
|
-
assert route.called
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@respx.mock
|
|
158
|
-
def test_auth_me(client):
|
|
159
|
-
mock_user = {
|
|
160
|
-
"id": "user-123",
|
|
161
|
-
"name": "Ata Can",
|
|
162
|
-
"username": "atacan",
|
|
163
|
-
"email": "ata@example.com",
|
|
164
|
-
"provider": "email",
|
|
165
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
166
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
167
|
-
}
|
|
168
|
-
route = respx.get(f"{BASE_URL}/api/user/me").mock(return_value=Response(200, json=mock_user))
|
|
169
|
-
|
|
170
|
-
user = client.auth.me()
|
|
171
|
-
assert isinstance(user, User)
|
|
172
|
-
assert user.id == "user-123"
|
|
173
|
-
assert route.called
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
@respx.mock
|
|
177
|
-
def test_authentication_error(client):
|
|
178
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
179
|
-
return_value=Response(401, json={"message": "Invalid API Key"})
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
with pytest.raises(AuthenticationError):
|
|
183
|
-
client.resumes.list()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@respx.mock
|
|
187
|
-
def test_not_found_error(client):
|
|
188
|
-
respx.get(f"{BASE_URL}/api/openapi/resume/non-existent").mock(
|
|
189
|
-
return_value=Response(404, json={"message": "Not found"})
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
with pytest.raises(NotFoundError):
|
|
193
|
-
client.resumes.get("non-existent")
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@respx.mock
|
|
197
|
-
def test_generic_api_error_500(client):
|
|
198
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
199
|
-
return_value=Response(500, text="Internal Server Error")
|
|
200
|
-
)
|
|
201
|
-
|
|
202
|
-
with pytest.raises(ReactiveResumeAPIError) as exc_info:
|
|
203
|
-
client.resumes.list()
|
|
204
|
-
assert exc_info.value.status_code == 500
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
@respx.mock
|
|
208
|
-
def test_network_error(client):
|
|
209
|
-
respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
210
|
-
side_effect=httpx.ConnectTimeout("Connection timed out")
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
with pytest.raises(ReactiveResumeError) as exc_info:
|
|
214
|
-
client.resumes.list()
|
|
215
|
-
assert "Network or connection error occurred" in str(exc_info.value)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def test_token_api_key_modifiers(client):
|
|
219
|
-
client.set_token("new-token")
|
|
220
|
-
assert client.client.headers["Authorization"] == "Bearer new-token"
|
|
221
|
-
assert "x-api-key" not in client.client.headers
|
|
222
|
-
|
|
223
|
-
client.set_api_key("new-api-key")
|
|
224
|
-
assert client.client.headers["x-api-key"] == "new-api-key"
|
|
225
|
-
assert "Authorization" not in client.client.headers
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def test_token_initialization():
|
|
229
|
-
with RxResumeClient(base_url=BASE_URL, token="initial-token") as c:
|
|
230
|
-
assert c.client.headers["Authorization"] == "Bearer initial-token"
|
|
231
|
-
assert "x-api-key" not in c.client.headers
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@respx.mock
|
|
235
|
-
def test_list_resumes_pagination(client):
|
|
236
|
-
mock_resume = {
|
|
237
|
-
"id": "paginated-resume-id",
|
|
238
|
-
"name": "Paginated CV",
|
|
239
|
-
"slug": "paginated-cv",
|
|
240
|
-
"userId": "user-123",
|
|
241
|
-
"visibility": "public",
|
|
242
|
-
"locked": False,
|
|
243
|
-
"data": {"basics": {}, "sections": {}},
|
|
244
|
-
"createdAt": "2026-07-13T12:00:00Z",
|
|
245
|
-
"updatedAt": "2026-07-13T12:00:00Z",
|
|
246
|
-
}
|
|
247
|
-
# Mock pagination wrapper {"data": [...]}
|
|
248
|
-
route = respx.get(f"{BASE_URL}/api/openapi/resumes").mock(
|
|
249
|
-
return_value=Response(200, json={"data": [mock_resume]})
|
|
250
|
-
)
|
|
251
|
-
resumes = client.resumes.list()
|
|
252
|
-
assert len(resumes) == 1
|
|
253
|
-
assert resumes[0].id == "paginated-resume-id"
|
|
254
|
-
assert route.called
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def test_exceptions_str_no_status_code():
|
|
258
|
-
err = ReactiveResumeAPIError("Something went wrong")
|
|
259
|
-
assert str(err) == "Something went wrong"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rxresume_python-0.2.0 → rxresume_python-0.3.0}/rxresume_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|