rxresume-python 0.1.0__py3-none-any.whl
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.
- reactive_resume/__init__.py +26 -0
- reactive_resume/api/__init__.py +11 -0
- reactive_resume/api/auth.py +66 -0
- reactive_resume/api/resumes.py +169 -0
- reactive_resume/core/__init__.py +17 -0
- reactive_resume/core/async_client.py +151 -0
- reactive_resume/core/client.py +151 -0
- reactive_resume/core/exceptions.py +45 -0
- reactive_resume/models/__init__.py +49 -0
- reactive_resume/models/resume.py +206 -0
- reactive_resume/models/user.py +24 -0
- rxresume_python-0.1.0.dist-info/METADATA +155 -0
- rxresume_python-0.1.0.dist-info/RECORD +16 -0
- rxresume_python-0.1.0.dist-info/WHEEL +5 -0
- rxresume_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- rxresume_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Reactive Resume API Python SDK.
|
|
2
|
+
|
|
3
|
+
An unofficial client for programmatically interacting with Reactive Resume v4 API.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core.client import RxResumeClient
|
|
7
|
+
from .core.async_client import AsyncRxResumeClient
|
|
8
|
+
from .core.exceptions import (
|
|
9
|
+
ReactiveResumeError,
|
|
10
|
+
ReactiveResumeAPIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
NotFoundError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"RxResumeClient",
|
|
20
|
+
"AsyncRxResumeClient",
|
|
21
|
+
"ReactiveResumeError",
|
|
22
|
+
"ReactiveResumeAPIError",
|
|
23
|
+
"AuthenticationError",
|
|
24
|
+
"NotFoundError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Authentication endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
from ..models.user import User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthAPI:
|
|
8
|
+
"""Synchronous Authentication operations."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client) -> None:
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
def login(self, email: str, password: str) -> Tuple[str, User]:
|
|
14
|
+
"""Log in with email and password.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
email: User's email.
|
|
18
|
+
password: User's password.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
A tuple of (access_token, User object).
|
|
22
|
+
"""
|
|
23
|
+
payload = {"identifier": email, "password": password}
|
|
24
|
+
response = self._client._request("POST", "/api/auth/login", json=payload)
|
|
25
|
+
|
|
26
|
+
# Typically returns token in cookies or response body
|
|
27
|
+
# Let's extract token and user
|
|
28
|
+
token = response.get("token") or response.get("accessToken", "")
|
|
29
|
+
user_data = response.get("user") or response
|
|
30
|
+
user = User.model_validate(user_data)
|
|
31
|
+
return token, user
|
|
32
|
+
|
|
33
|
+
def me(self) -> User:
|
|
34
|
+
"""Get the current authenticated user profile."""
|
|
35
|
+
response = self._client._request("GET", "/api/user/me")
|
|
36
|
+
return User.model_validate(response)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncAuthAPI:
|
|
40
|
+
"""Asynchronous Authentication operations."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, client) -> None:
|
|
43
|
+
self._client = client
|
|
44
|
+
|
|
45
|
+
async def login(self, email: str, password: str) -> Tuple[str, User]:
|
|
46
|
+
"""Log in asynchronously with email and password.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
email: User's email.
|
|
50
|
+
password: User's password.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A tuple of (access_token, User object).
|
|
54
|
+
"""
|
|
55
|
+
payload = {"identifier": email, "password": password}
|
|
56
|
+
response = await self._client._request("POST", "/api/auth/login", json=payload)
|
|
57
|
+
|
|
58
|
+
token = response.get("token") or response.get("accessToken", "")
|
|
59
|
+
user_data = response.get("user") or response
|
|
60
|
+
user = User.model_validate(user_data)
|
|
61
|
+
return token, user
|
|
62
|
+
|
|
63
|
+
async def me(self) -> User:
|
|
64
|
+
"""Get the current authenticated user profile asynchronously."""
|
|
65
|
+
response = await self._client._request("GET", "/api/user/me")
|
|
66
|
+
return User.model_validate(response)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Resumes endpoints implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from ..models.resume import Resume, ResumeImportData
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResumesAPI:
|
|
8
|
+
"""Synchronous Resume operations."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client) -> None:
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
def list(self) -> List[Resume]:
|
|
14
|
+
"""List all resumes for the authenticated user.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A list of Resume objects.
|
|
18
|
+
"""
|
|
19
|
+
response = self._client._request("GET", "/api/openapi/resumes")
|
|
20
|
+
# The response is usually a list of resume objects
|
|
21
|
+
if isinstance(response, dict) and "data" in response:
|
|
22
|
+
# Handle potential pagination wrapper
|
|
23
|
+
items = response["data"]
|
|
24
|
+
else:
|
|
25
|
+
items = response
|
|
26
|
+
return [Resume.model_validate(item) for item in items]
|
|
27
|
+
|
|
28
|
+
def get(self, resume_id: str) -> Resume:
|
|
29
|
+
"""Retrieve a specific resume by ID.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
resume_id: Unique identifier for the resume.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The Resume object.
|
|
36
|
+
"""
|
|
37
|
+
response = self._client._request("GET", f"/api/openapi/resume/{resume_id}")
|
|
38
|
+
return Resume.model_validate(response)
|
|
39
|
+
|
|
40
|
+
def create(self, data: ResumeImportData) -> Resume:
|
|
41
|
+
"""Create or import a new resume.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
data: ResumeImportData object containing title, basics, and sections.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The created Resume object.
|
|
48
|
+
"""
|
|
49
|
+
# Convert Pydantic model to dict, matching what API expects
|
|
50
|
+
payload = data.model_dump(by_alias=True, exclude_none=True)
|
|
51
|
+
response = self._client._request("POST", "/api/openapi/resumes", json=payload)
|
|
52
|
+
return Resume.model_validate(response)
|
|
53
|
+
|
|
54
|
+
def import_resume(self, data: ResumeImportData) -> Resume:
|
|
55
|
+
"""Alias for create, importing a resume.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
data: ResumeImportData object containing title, basics, and sections.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The imported Resume object.
|
|
62
|
+
"""
|
|
63
|
+
return self.create(data)
|
|
64
|
+
|
|
65
|
+
def update(self, resume_id: str, data: Dict[str, Any]) -> Resume:
|
|
66
|
+
"""Update a resume (PATCH).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
resume_id: Unique identifier for the resume.
|
|
70
|
+
data: Dictionary of properties to update (e.g. JSON Patch or key-value updates).
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The updated Resume object.
|
|
74
|
+
"""
|
|
75
|
+
# Reactive Resume v4 supports PATCH with JSON Patch or direct body updates
|
|
76
|
+
response = self._client._request("PATCH", f"/api/openapi/resume/{resume_id}", json=data)
|
|
77
|
+
return Resume.model_validate(response)
|
|
78
|
+
|
|
79
|
+
def delete(self, resume_id: str) -> None:
|
|
80
|
+
"""Delete a resume by ID.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
resume_id: Unique identifier for the resume.
|
|
84
|
+
"""
|
|
85
|
+
self._client._request("DELETE", f"/api/openapi/resume/{resume_id}")
|
|
86
|
+
|
|
87
|
+
def get_pdf_url(self, resume_id: str) -> str:
|
|
88
|
+
"""Get the URL to download the PDF for a specific resume.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
resume_id: Unique identifier for the resume.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The absolute PDF download URL.
|
|
95
|
+
"""
|
|
96
|
+
# The print/pdf download URL format in Reactive Resume V4 API
|
|
97
|
+
# Typically of form: {base_url}/api/openapi/resume/{resume_id}/pdf
|
|
98
|
+
return f"{self._client.base_url}/api/openapi/resume/{resume_id}/pdf"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class AsyncResumesAPI:
|
|
102
|
+
"""Asynchronous Resume operations."""
|
|
103
|
+
|
|
104
|
+
def __init__(self, client) -> None:
|
|
105
|
+
self._client = client
|
|
106
|
+
|
|
107
|
+
async def list(self) -> List[Resume]:
|
|
108
|
+
"""List all resumes asynchronously.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A list of Resume objects.
|
|
112
|
+
"""
|
|
113
|
+
response = await self._client._request("GET", "/api/openapi/resumes")
|
|
114
|
+
if isinstance(response, dict) and "data" in response:
|
|
115
|
+
items = response["data"]
|
|
116
|
+
else:
|
|
117
|
+
items = response
|
|
118
|
+
return [Resume.model_validate(item) for item in items]
|
|
119
|
+
|
|
120
|
+
async def get(self, resume_id: str) -> Resume:
|
|
121
|
+
"""Retrieve a specific resume asynchronously.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
resume_id: Unique identifier for the resume.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The Resume object.
|
|
128
|
+
"""
|
|
129
|
+
response = await self._client._request("GET", f"/api/openapi/resume/{resume_id}")
|
|
130
|
+
return Resume.model_validate(response)
|
|
131
|
+
|
|
132
|
+
async def create(self, data: ResumeImportData) -> Resume:
|
|
133
|
+
"""Create or import a new resume asynchronously.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
data: ResumeImportData object.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The created Resume object.
|
|
140
|
+
"""
|
|
141
|
+
payload = data.model_dump(by_alias=True, exclude_none=True)
|
|
142
|
+
response = await self._client._request("POST", "/api/openapi/resumes", json=payload)
|
|
143
|
+
return Resume.model_validate(response)
|
|
144
|
+
|
|
145
|
+
async def import_resume(self, data: ResumeImportData) -> Resume:
|
|
146
|
+
"""Alias for create, importing a resume asynchronously."""
|
|
147
|
+
return await self.create(data)
|
|
148
|
+
|
|
149
|
+
async def update(self, resume_id: str, data: Dict[str, Any]) -> Resume:
|
|
150
|
+
"""Update a resume asynchronously."""
|
|
151
|
+
response = await self._client._request(
|
|
152
|
+
"PATCH", f"/api/openapi/resume/{resume_id}", json=data
|
|
153
|
+
)
|
|
154
|
+
return Resume.model_validate(response)
|
|
155
|
+
|
|
156
|
+
async def delete(self, resume_id: str) -> None:
|
|
157
|
+
"""Delete a resume asynchronously."""
|
|
158
|
+
await self._client._request("DELETE", f"/api/openapi/resume/{resume_id}")
|
|
159
|
+
|
|
160
|
+
async def get_pdf_url(self, resume_id: str) -> str:
|
|
161
|
+
"""Get the URL to download the PDF asynchronously.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
resume_id: Unique identifier for the resume.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
The absolute PDF download URL.
|
|
168
|
+
"""
|
|
169
|
+
return f"{self._client.base_url}/api/openapi/resume/{resume_id}/pdf"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Core functionality and HTTP client structures."""
|
|
2
|
+
|
|
3
|
+
from .exceptions import (
|
|
4
|
+
ReactiveResumeError,
|
|
5
|
+
ReactiveResumeAPIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
ValidationError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ReactiveResumeError",
|
|
13
|
+
"ReactiveResumeAPIError",
|
|
14
|
+
"AuthenticationError",
|
|
15
|
+
"NotFoundError",
|
|
16
|
+
"ValidationError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Asynchronous API Client implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
ReactiveResumeError,
|
|
8
|
+
ReactiveResumeAPIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
)
|
|
12
|
+
from ..api.auth import AsyncAuthAPI
|
|
13
|
+
from ..api.resumes import AsyncResumesAPI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncRxResumeClient:
|
|
17
|
+
"""Asynchronous client for Reactive Resume v4 API."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str,
|
|
22
|
+
api_key: Optional[str] = None,
|
|
23
|
+
token: Optional[str] = None,
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
verify: bool = True,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize the client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
base_url: The base URL of the Reactive Resume instance.
|
|
31
|
+
api_key: API Key for x-api-key authentication.
|
|
32
|
+
token: Bearer Token for Authorization authentication.
|
|
33
|
+
timeout: Request timeout in seconds.
|
|
34
|
+
verify: Verify SSL certificates.
|
|
35
|
+
"""
|
|
36
|
+
self.base_url = base_url.rstrip("/")
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.verify = verify
|
|
39
|
+
|
|
40
|
+
headers = {
|
|
41
|
+
"Accept": "application/json",
|
|
42
|
+
"User-Agent": "rxresume-python/0.1.0",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if api_key:
|
|
46
|
+
headers["x-api-key"] = api_key
|
|
47
|
+
elif token:
|
|
48
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
49
|
+
|
|
50
|
+
self.client = httpx.AsyncClient(
|
|
51
|
+
base_url=self.base_url,
|
|
52
|
+
headers=headers,
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
verify=self.verify,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Initialize API sections
|
|
58
|
+
self.auth = AsyncAuthAPI(self)
|
|
59
|
+
self.resumes = AsyncResumesAPI(self)
|
|
60
|
+
|
|
61
|
+
def set_token(self, token: str) -> None:
|
|
62
|
+
"""Update client headers with a new Bearer token."""
|
|
63
|
+
self.client.headers["Authorization"] = f"Bearer {token}"
|
|
64
|
+
if "x-api-key" in self.client.headers:
|
|
65
|
+
del self.client.headers["x-api-key"]
|
|
66
|
+
|
|
67
|
+
def set_api_key(self, api_key: str) -> None:
|
|
68
|
+
"""Update client headers with a new API key."""
|
|
69
|
+
self.client.headers["x-api-key"] = api_key
|
|
70
|
+
if "Authorization" in self.client.headers:
|
|
71
|
+
del self.client.headers["Authorization"]
|
|
72
|
+
|
|
73
|
+
async def _request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
url: str,
|
|
77
|
+
**kwargs: Any,
|
|
78
|
+
) -> Any:
|
|
79
|
+
"""Perform an asynchronous HTTP request and handle errors.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
method: HTTP method (GET, POST, etc.).
|
|
83
|
+
url: Target URL path (appended to base_url).
|
|
84
|
+
**kwargs: Extra arguments for httpx request.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Decoded JSON response or raw content.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
response = await self.client.request(method, url, **kwargs)
|
|
91
|
+
return self._handle_response(response)
|
|
92
|
+
except httpx.HTTPError as e:
|
|
93
|
+
if not isinstance(e, httpx.HTTPStatusError):
|
|
94
|
+
raise ReactiveResumeError(f"Network or connection error occurred: {e}") from e
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
98
|
+
"""Process response data or raise descriptive exceptions.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
response: Response object from httpx.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
except httpx.HTTPStatusError as e:
|
|
106
|
+
status_code = response.status_code
|
|
107
|
+
error_data = None
|
|
108
|
+
try:
|
|
109
|
+
error_data = response.json()
|
|
110
|
+
except ValueError:
|
|
111
|
+
error_data = response.text
|
|
112
|
+
|
|
113
|
+
message = ""
|
|
114
|
+
if isinstance(error_data, dict):
|
|
115
|
+
message = error_data.get("message") or error_data.get("error") or str(error_data)
|
|
116
|
+
else:
|
|
117
|
+
message = str(error_data)
|
|
118
|
+
|
|
119
|
+
if status_code in (401, 403):
|
|
120
|
+
raise AuthenticationError(
|
|
121
|
+
f"Authentication failed: {message}",
|
|
122
|
+
status_code=status_code,
|
|
123
|
+
response_body=error_data,
|
|
124
|
+
) from e
|
|
125
|
+
elif status_code == 404:
|
|
126
|
+
raise NotFoundError(
|
|
127
|
+
f"Resource not found: {message}",
|
|
128
|
+
status_code=status_code,
|
|
129
|
+
response_body=error_data,
|
|
130
|
+
) from e
|
|
131
|
+
else:
|
|
132
|
+
raise ReactiveResumeAPIError(
|
|
133
|
+
f"API error: {message}",
|
|
134
|
+
status_code=status_code,
|
|
135
|
+
response_body=error_data,
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
# Success path
|
|
139
|
+
if response.headers.get("content-type", "").startswith("application/json"):
|
|
140
|
+
return response.json()
|
|
141
|
+
return response.content
|
|
142
|
+
|
|
143
|
+
async def close(self) -> None:
|
|
144
|
+
"""Close the underlying HTTP client."""
|
|
145
|
+
await self.client.aclose()
|
|
146
|
+
|
|
147
|
+
async def __aenter__(self) -> "AsyncRxResumeClient":
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
151
|
+
await self.close()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Synchronous API Client implementation."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
ReactiveResumeError,
|
|
8
|
+
ReactiveResumeAPIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
)
|
|
12
|
+
from ..api.auth import AuthAPI
|
|
13
|
+
from ..api.resumes import ResumesAPI
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RxResumeClient:
|
|
17
|
+
"""Synchronous client for Reactive Resume v4 API."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str,
|
|
22
|
+
api_key: Optional[str] = None,
|
|
23
|
+
token: Optional[str] = None,
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
verify: bool = True,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize the client.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
base_url: The base URL of the Reactive Resume instance.
|
|
31
|
+
api_key: API Key for x-api-key authentication.
|
|
32
|
+
token: Bearer Token for Authorization authentication.
|
|
33
|
+
timeout: Request timeout in seconds.
|
|
34
|
+
verify: Verify SSL certificates.
|
|
35
|
+
"""
|
|
36
|
+
self.base_url = base_url.rstrip("/")
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.verify = verify
|
|
39
|
+
|
|
40
|
+
headers = {
|
|
41
|
+
"Accept": "application/json",
|
|
42
|
+
"User-Agent": "rxresume-python/0.1.0",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if api_key:
|
|
46
|
+
headers["x-api-key"] = api_key
|
|
47
|
+
elif token:
|
|
48
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
49
|
+
|
|
50
|
+
self.client = httpx.Client(
|
|
51
|
+
base_url=self.base_url,
|
|
52
|
+
headers=headers,
|
|
53
|
+
timeout=self.timeout,
|
|
54
|
+
verify=self.verify,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Initialize API sections
|
|
58
|
+
self.auth = AuthAPI(self)
|
|
59
|
+
self.resumes = ResumesAPI(self)
|
|
60
|
+
|
|
61
|
+
def set_token(self, token: str) -> None:
|
|
62
|
+
"""Update client headers with a new Bearer token."""
|
|
63
|
+
self.client.headers["Authorization"] = f"Bearer {token}"
|
|
64
|
+
if "x-api-key" in self.client.headers:
|
|
65
|
+
del self.client.headers["x-api-key"]
|
|
66
|
+
|
|
67
|
+
def set_api_key(self, api_key: str) -> None:
|
|
68
|
+
"""Update client headers with a new API key."""
|
|
69
|
+
self.client.headers["x-api-key"] = api_key
|
|
70
|
+
if "Authorization" in self.client.headers:
|
|
71
|
+
del self.client.headers["Authorization"]
|
|
72
|
+
|
|
73
|
+
def _request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
url: str,
|
|
77
|
+
**kwargs: Any,
|
|
78
|
+
) -> Any:
|
|
79
|
+
"""Perform a synchronous HTTP request and handle errors.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
method: HTTP method (GET, POST, etc.).
|
|
83
|
+
url: Target URL path (appended to base_url).
|
|
84
|
+
**kwargs: Extra arguments for httpx request.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Decoded JSON response or raw content.
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
response = self.client.request(method, url, **kwargs)
|
|
91
|
+
return self._handle_response(response)
|
|
92
|
+
except httpx.HTTPError as e:
|
|
93
|
+
if not isinstance(e, httpx.HTTPStatusError):
|
|
94
|
+
raise ReactiveResumeError(f"Network or connection error occurred: {e}") from e
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
def _handle_response(self, response: httpx.Response) -> Any:
|
|
98
|
+
"""Process response data or raise descriptive exceptions.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
response: Response object from httpx.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
except httpx.HTTPStatusError as e:
|
|
106
|
+
status_code = response.status_code
|
|
107
|
+
error_data = None
|
|
108
|
+
try:
|
|
109
|
+
error_data = response.json()
|
|
110
|
+
except ValueError:
|
|
111
|
+
error_data = response.text
|
|
112
|
+
|
|
113
|
+
message = ""
|
|
114
|
+
if isinstance(error_data, dict):
|
|
115
|
+
message = error_data.get("message") or error_data.get("error") or str(error_data)
|
|
116
|
+
else:
|
|
117
|
+
message = str(error_data)
|
|
118
|
+
|
|
119
|
+
if status_code in (401, 403):
|
|
120
|
+
raise AuthenticationError(
|
|
121
|
+
f"Authentication failed: {message}",
|
|
122
|
+
status_code=status_code,
|
|
123
|
+
response_body=error_data,
|
|
124
|
+
) from e
|
|
125
|
+
elif status_code == 404:
|
|
126
|
+
raise NotFoundError(
|
|
127
|
+
f"Resource not found: {message}",
|
|
128
|
+
status_code=status_code,
|
|
129
|
+
response_body=error_data,
|
|
130
|
+
) from e
|
|
131
|
+
else:
|
|
132
|
+
raise ReactiveResumeAPIError(
|
|
133
|
+
f"API error: {message}",
|
|
134
|
+
status_code=status_code,
|
|
135
|
+
response_body=error_data,
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
# Success path
|
|
139
|
+
if response.headers.get("content-type", "").startswith("application/json"):
|
|
140
|
+
return response.json()
|
|
141
|
+
return response.content
|
|
142
|
+
|
|
143
|
+
def close(self) -> None:
|
|
144
|
+
"""Close the underlying HTTP client."""
|
|
145
|
+
self.client.close()
|
|
146
|
+
|
|
147
|
+
def __enter__(self) -> "RxResumeClient":
|
|
148
|
+
return self
|
|
149
|
+
|
|
150
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
151
|
+
self.close()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Custom exceptions for the Reactive Resume API Client."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ReactiveResumeError(Exception):
|
|
7
|
+
"""Base exception for all Reactive Resume client errors."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidationError(ReactiveResumeError):
|
|
13
|
+
"""Raised when request data fails client-side validation."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ReactiveResumeAPIError(ReactiveResumeError):
|
|
19
|
+
"""Raised when the Reactive Resume API returns an error response."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
message: str,
|
|
24
|
+
status_code: Optional[int] = None,
|
|
25
|
+
response_body: Optional[Any] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.response_body = response_body
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
code_str = f" [Status {self.status_code}]" if self.status_code else ""
|
|
33
|
+
return f"{super().__str__()}{code_str}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(ReactiveResumeAPIError):
|
|
37
|
+
"""Raised when authentication fails (401/403)."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NotFoundError(ReactiveResumeAPIError):
|
|
43
|
+
"""Raised when a requested resource is not found (404)."""
|
|
44
|
+
|
|
45
|
+
pass
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Models module for Pydantic schema validation."""
|
|
2
|
+
|
|
3
|
+
from .user import User
|
|
4
|
+
from .resume import (
|
|
5
|
+
URLModel,
|
|
6
|
+
Profile,
|
|
7
|
+
Basics,
|
|
8
|
+
Item,
|
|
9
|
+
WorkItem,
|
|
10
|
+
EducationItem,
|
|
11
|
+
ProjectItem,
|
|
12
|
+
SkillItem,
|
|
13
|
+
LanguageItem,
|
|
14
|
+
CertificationItem,
|
|
15
|
+
AwardItem,
|
|
16
|
+
InterestItem,
|
|
17
|
+
ReferenceItem,
|
|
18
|
+
PublicationItem,
|
|
19
|
+
VolunteerItem,
|
|
20
|
+
CustomItem,
|
|
21
|
+
Section,
|
|
22
|
+
ResumeData,
|
|
23
|
+
Resume,
|
|
24
|
+
ResumeImportData,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"User",
|
|
29
|
+
"URLModel",
|
|
30
|
+
"Profile",
|
|
31
|
+
"Basics",
|
|
32
|
+
"Item",
|
|
33
|
+
"WorkItem",
|
|
34
|
+
"EducationItem",
|
|
35
|
+
"ProjectItem",
|
|
36
|
+
"SkillItem",
|
|
37
|
+
"LanguageItem",
|
|
38
|
+
"CertificationItem",
|
|
39
|
+
"AwardItem",
|
|
40
|
+
"InterestItem",
|
|
41
|
+
"ReferenceItem",
|
|
42
|
+
"PublicationItem",
|
|
43
|
+
"VolunteerItem",
|
|
44
|
+
"CustomItem",
|
|
45
|
+
"Section",
|
|
46
|
+
"ResumeData",
|
|
47
|
+
"Resume",
|
|
48
|
+
"ResumeImportData",
|
|
49
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Pydantic models representing resume data in Reactive Resume."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional, List, Dict, Any, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class URLModel(BaseModel):
|
|
9
|
+
"""Represents a URL structure in Reactive Resume."""
|
|
10
|
+
|
|
11
|
+
label: str = ""
|
|
12
|
+
href: str = ""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Profile(BaseModel):
|
|
16
|
+
"""Represents a social media profile link."""
|
|
17
|
+
|
|
18
|
+
id: Optional[str] = None
|
|
19
|
+
network: str = ""
|
|
20
|
+
username: str = ""
|
|
21
|
+
url: Union[URLModel, str] = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Basics(BaseModel):
|
|
25
|
+
"""Represents the basic personal information of a candidate."""
|
|
26
|
+
|
|
27
|
+
name: str = ""
|
|
28
|
+
headline: str = ""
|
|
29
|
+
email: str = ""
|
|
30
|
+
phone: str = ""
|
|
31
|
+
website: Union[URLModel, str] = ""
|
|
32
|
+
location: str = ""
|
|
33
|
+
picture: str = ""
|
|
34
|
+
profiles: List[Profile] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# General item model for list items
|
|
38
|
+
class Item(BaseModel):
|
|
39
|
+
"""Base model for list items within sections."""
|
|
40
|
+
|
|
41
|
+
id: str
|
|
42
|
+
visible: bool = True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WorkItem(Item):
|
|
46
|
+
"""Represents a work experience entry."""
|
|
47
|
+
|
|
48
|
+
company: str = ""
|
|
49
|
+
position: str = ""
|
|
50
|
+
location: str = ""
|
|
51
|
+
date: str = ""
|
|
52
|
+
summary: str = ""
|
|
53
|
+
url: Union[URLModel, str] = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class EducationItem(Item):
|
|
57
|
+
"""Represents an education entry."""
|
|
58
|
+
|
|
59
|
+
institution: str = ""
|
|
60
|
+
study_type: str = Field("", alias="studyType")
|
|
61
|
+
area: str = ""
|
|
62
|
+
score: str = ""
|
|
63
|
+
date: str = ""
|
|
64
|
+
summary: str = ""
|
|
65
|
+
url: Union[URLModel, str] = ""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProjectItem(Item):
|
|
71
|
+
"""Represents a project entry."""
|
|
72
|
+
|
|
73
|
+
name: str = ""
|
|
74
|
+
description: str = ""
|
|
75
|
+
date: str = ""
|
|
76
|
+
summary: str = ""
|
|
77
|
+
keywords: List[str] = Field(default_factory=list)
|
|
78
|
+
url: Union[URLModel, str] = ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SkillItem(Item):
|
|
82
|
+
"""Represents a skill entry."""
|
|
83
|
+
|
|
84
|
+
name: str = ""
|
|
85
|
+
description: str = ""
|
|
86
|
+
level: str = ""
|
|
87
|
+
keywords: List[str] = Field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LanguageItem(Item):
|
|
91
|
+
"""Represents a language entry."""
|
|
92
|
+
|
|
93
|
+
name: str = ""
|
|
94
|
+
description: str = ""
|
|
95
|
+
level: str = ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CertificationItem(Item):
|
|
99
|
+
"""Represents a certification entry."""
|
|
100
|
+
|
|
101
|
+
name: str = ""
|
|
102
|
+
issuer: str = ""
|
|
103
|
+
date: str = ""
|
|
104
|
+
summary: str = ""
|
|
105
|
+
url: Union[URLModel, str] = ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AwardItem(Item):
|
|
109
|
+
"""Represents an award or honor entry."""
|
|
110
|
+
|
|
111
|
+
title: str = ""
|
|
112
|
+
awarder: str = ""
|
|
113
|
+
date: str = ""
|
|
114
|
+
summary: str = ""
|
|
115
|
+
url: Union[URLModel, str] = ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class InterestItem(Item):
|
|
119
|
+
"""Represents an interest entry."""
|
|
120
|
+
|
|
121
|
+
name: str = ""
|
|
122
|
+
keywords: List[str] = Field(default_factory=list)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ReferenceItem(Item):
|
|
126
|
+
"""Represents a reference entry."""
|
|
127
|
+
|
|
128
|
+
name: str = ""
|
|
129
|
+
relationship: str = ""
|
|
130
|
+
summary: str = ""
|
|
131
|
+
url: Union[URLModel, str] = ""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PublicationItem(Item):
|
|
135
|
+
"""Represents a publication entry."""
|
|
136
|
+
|
|
137
|
+
name: str = ""
|
|
138
|
+
publisher: str = ""
|
|
139
|
+
date: str = ""
|
|
140
|
+
summary: str = ""
|
|
141
|
+
url: Union[URLModel, str] = ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class VolunteerItem(Item):
|
|
145
|
+
"""Represents a volunteer work entry."""
|
|
146
|
+
|
|
147
|
+
organization: str = ""
|
|
148
|
+
position: str = ""
|
|
149
|
+
location: str = ""
|
|
150
|
+
date: str = ""
|
|
151
|
+
summary: str = ""
|
|
152
|
+
url: Union[URLModel, str] = ""
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class CustomItem(Item):
|
|
156
|
+
"""Represents a custom item entry in custom sections."""
|
|
157
|
+
|
|
158
|
+
title: str = ""
|
|
159
|
+
subtitle: str = ""
|
|
160
|
+
date: str = ""
|
|
161
|
+
summary: str = ""
|
|
162
|
+
url: Union[URLModel, str] = ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Section Models
|
|
166
|
+
class Section(BaseModel):
|
|
167
|
+
"""Base schema for a resume section."""
|
|
168
|
+
|
|
169
|
+
id: str
|
|
170
|
+
name: str
|
|
171
|
+
columns: int = 1
|
|
172
|
+
visible: bool = True
|
|
173
|
+
items: List[Any] = Field(default_factory=list)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ResumeData(BaseModel):
|
|
177
|
+
"""Contains all actual resume sections and basic details."""
|
|
178
|
+
|
|
179
|
+
basics: Basics = Field(default_factory=Basics)
|
|
180
|
+
sections: Dict[str, Section] = Field(default_factory=dict)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Resume(BaseModel):
|
|
184
|
+
"""Represents a complete resume object returned by the API."""
|
|
185
|
+
|
|
186
|
+
id: str
|
|
187
|
+
name: str
|
|
188
|
+
slug: str
|
|
189
|
+
user_id: str = Field(..., alias="userId")
|
|
190
|
+
visibility: str = "public"
|
|
191
|
+
locked: bool = False
|
|
192
|
+
data: ResumeData
|
|
193
|
+
created_at: datetime = Field(..., alias="createdAt")
|
|
194
|
+
updated_at: datetime = Field(..., alias="updatedAt")
|
|
195
|
+
|
|
196
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ResumeImportData(BaseModel):
|
|
200
|
+
"""Schema for importing/creating a new resume."""
|
|
201
|
+
|
|
202
|
+
title: str = Field(..., description="The name/title of the resume")
|
|
203
|
+
slug: Optional[str] = Field(None, description="Optional custom URL slug")
|
|
204
|
+
basics: Optional[Basics] = None
|
|
205
|
+
sections: Optional[Dict[str, Section]] = None
|
|
206
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Pydantic models representing user accounts in Reactive Resume."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, EmailStr, Field, ConfigDict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class User(BaseModel):
|
|
8
|
+
"""Represents a user account in Reactive Resume."""
|
|
9
|
+
|
|
10
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
11
|
+
|
|
12
|
+
id: str = Field(..., description="Unique identifier for the user")
|
|
13
|
+
name: str = Field(..., description="Full name of the user")
|
|
14
|
+
username: str = Field(..., description="Username of the user")
|
|
15
|
+
email: EmailStr = Field(..., description="Email address of the user")
|
|
16
|
+
provider: str = Field(
|
|
17
|
+
"email", description="Authentication provider (e.g., email, github, google)"
|
|
18
|
+
)
|
|
19
|
+
created_at: datetime = Field(
|
|
20
|
+
..., alias="createdAt", description="Timestamp when the user was created"
|
|
21
|
+
)
|
|
22
|
+
updated_at: datetime = Field(
|
|
23
|
+
..., alias="updatedAt", description="Timestamp when the user was last updated"
|
|
24
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rxresume-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Python API client (SDK) for Reactive Resume v4
|
|
5
|
+
Author: Ata Can Yaymacı
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: httpx>=0.24.0
|
|
14
|
+
Requires-Dist: pydantic[email]>=2.0.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
|
|
18
|
+
Requires-Dist: respx>=0.20.0; extra == "dev"
|
|
19
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|
21
|
+
Requires-Dist: bandit>=1.7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Reactive Resume Python SDK (`rxresume-python`)
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/downloads/)
|
|
28
|
+
[](https://docs.pydantic.dev/)
|
|
29
|
+
[](https://www.python-httpx.org/)
|
|
30
|
+
|
|
31
|
+
An unofficial modern, type-safe Python API Client (SDK) for **Reactive Resume v4**.
|
|
32
|
+
|
|
33
|
+
It supports both synchronous (`httpx.Client`) and asynchronous (`httpx.AsyncClient`) clients, ensures fully typed request/response schemas using **Pydantic v2**, and maps API errors to clean, descriptive Python exceptions.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Dual client modes**: Support for both sync and async APIs.
|
|
40
|
+
- **Type safety**: Fully typed models for Resumes, Sections, Basics, and Users using Pydantic V2.
|
|
41
|
+
- **Robust error handling**: Raw API status errors are automatically parsed into specific exceptions (`AuthenticationError`, `NotFoundError`, etc.).
|
|
42
|
+
- **Developer Experience (DX)**: Code-completion ready with clear typing and docstrings.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Install the package via `pip` or your favorite package manager:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install rxresume-python
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### 1. Asynchronous Client (FastAPI / Asynchronous Code)
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from reactive_resume import AsyncRxResumeClient
|
|
63
|
+
from reactive_resume.models import ResumeImportData, Basics
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# Initialize the async client
|
|
67
|
+
async with AsyncRxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as client:
|
|
68
|
+
# Create a new resume
|
|
69
|
+
import_data = ResumeImportData(
|
|
70
|
+
title="Ata Can Yaymacı - Backend Engineer",
|
|
71
|
+
basics=Basics(
|
|
72
|
+
name="Ata Can Yaymacı",
|
|
73
|
+
headline="Backend Engineer",
|
|
74
|
+
email="ata@example.com",
|
|
75
|
+
phone="+905555555555",
|
|
76
|
+
website="https://example.com"
|
|
77
|
+
),
|
|
78
|
+
sections={}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Import/Create a new resume
|
|
83
|
+
new_resume = await client.resumes.import_resume(import_data)
|
|
84
|
+
print(f"Created resume: {new_resume.name} (ID: {new_resume.id})")
|
|
85
|
+
|
|
86
|
+
# Fetch the generated PDF URL
|
|
87
|
+
pdf_url = await client.resumes.get_pdf_url(new_resume.id)
|
|
88
|
+
print(f"PDF URL: {pdf_url}")
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"An error occurred: {e}")
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Synchronous Client
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from reactive_resume import RxResumeClient
|
|
101
|
+
from reactive_resume.models import ResumeImportData
|
|
102
|
+
|
|
103
|
+
with RxResumeClient(base_url="https://rxresu.me", api_key="your_api_key") as client:
|
|
104
|
+
resumes = client.resumes.list()
|
|
105
|
+
for resume in resumes:
|
|
106
|
+
print(f"Resume: {resume.name} (Slug: {resume.slug})")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Error Handling
|
|
112
|
+
|
|
113
|
+
All client API calls map HTTP errors to specific exception classes:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from reactive_resume import RxResumeClient, AuthenticationError, NotFoundError
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with RxResumeClient(base_url="https://rxresu.me", api_key="wrong_key") as client:
|
|
120
|
+
client.resumes.list()
|
|
121
|
+
except AuthenticationError as e:
|
|
122
|
+
print(f"Auth error (Status {e.status_code}): {e}")
|
|
123
|
+
except NotFoundError as e:
|
|
124
|
+
print(f"Not found: {e}")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print(f"Generic error: {e}")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Development & Testing
|
|
132
|
+
|
|
133
|
+
1. Clone the repository:
|
|
134
|
+
```bash
|
|
135
|
+
git clone https://github.com/your-username/reactive-resume-api-client-py.git
|
|
136
|
+
cd reactive-resume-api-client-py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
2. Install dependencies:
|
|
140
|
+
```bash
|
|
141
|
+
python3 -m venv .venv
|
|
142
|
+
source .venv/bin/activate
|
|
143
|
+
pip install -e ".[dev]"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
3. Run the tests:
|
|
147
|
+
```bash
|
|
148
|
+
pytest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
reactive_resume/__init__.py,sha256=F5Ze9bu2L1pwIrtYLnXAXeV6cL2p72xDy5I8qnJQfPM,581
|
|
2
|
+
reactive_resume/api/__init__.py,sha256=t4sq_czaOLuPxyMhnXFoFTAWsr3qidF5i3RTlESRn8M,221
|
|
3
|
+
reactive_resume/api/auth.py,sha256=YkuUjWSOEhxmuY2znOmLf9b2i915I4seNblkk1icBvI,2177
|
|
4
|
+
reactive_resume/api/resumes.py,sha256=fiysbrwWxIccYwSzmuI_5KdrPVSkaPLTTKMf0c-Im4c,5701
|
|
5
|
+
reactive_resume/core/__init__.py,sha256=TXvcDN-AFX5ctGXoaqHck1eoi0jxLLBCh5J-6ySFZe8,343
|
|
6
|
+
reactive_resume/core/async_client.py,sha256=n779xddhVoWhzta3_SdDom0hRPBH5sbH20xR02B5j7Y,4866
|
|
7
|
+
reactive_resume/core/client.py,sha256=XfPqleKefk7A0VxXjSydWIWuwwuPMFdEfzJUrmaf_Ps,4782
|
|
8
|
+
reactive_resume/core/exceptions.py,sha256=cFOPfpmowDu4D5YSLkpnH0brUyTAvwydnwRg1-bpGl4,1124
|
|
9
|
+
reactive_resume/models/__init__.py,sha256=OFiJN0Aep4gKOKn0vVSg6AzMDL8_hvWi3ZazE5ys0u4,819
|
|
10
|
+
reactive_resume/models/resume.py,sha256=BoBkk77v_eEBIrHUCqBiYQrQEsm3VXUtIc1bQpn8hrY,4620
|
|
11
|
+
reactive_resume/models/user.py,sha256=uR8NS0R4Dca63cEh40TjJlrgtDuPmO6TKE6VEvCKTUU,939
|
|
12
|
+
rxresume_python-0.1.0.dist-info/licenses/LICENSE,sha256=Mlna-n1TpPV1TDC0tRQe0QsOMi2-5jTgvnypocbGghw,1073
|
|
13
|
+
rxresume_python-0.1.0.dist-info/METADATA,sha256=_OdWwgUifd71EhyNRen4B1GTZuPkbO8rFnj5xPwTQTg,4647
|
|
14
|
+
rxresume_python-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
15
|
+
rxresume_python-0.1.0.dist-info/top_level.txt,sha256=XqM8_KWS_Y5kNXEG18kAShCVtNwErO1NfCeCZ1u-INo,16
|
|
16
|
+
rxresume_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ata Can Yaymacı
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
reactive_resume
|