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.
@@ -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,11 @@
1
+ """API endpoints and service groups."""
2
+
3
+ from .auth import AuthAPI, AsyncAuthAPI
4
+ from .resumes import ResumesAPI, AsyncResumesAPI
5
+
6
+ __all__ = [
7
+ "AuthAPI",
8
+ "AsyncAuthAPI",
9
+ "ResumesAPI",
10
+ "AsyncResumesAPI",
11
+ ]
@@ -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
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
28
+ [![Pydantic v2](https://img.shields.io/badge/pydantic-v2-red.svg)](https://docs.pydantic.dev/)
29
+ [![HTTPX](https://img.shields.io/badge/http--client-httpx-brightgreen.svg)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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