curious-pyapi 0.1.0__tar.gz → 0.1.2__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.
Files changed (30) hide show
  1. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/.github/workflows/publish_to_pypi.yaml +3 -3
  2. curious_pyapi-0.1.2/.markdownlint.yaml +5 -0
  3. curious_pyapi-0.1.2/.vscode/settings.json +9 -0
  4. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/CHANGELOG.md +14 -0
  5. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/PKG-INFO +1 -1
  6. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/pyproject.toml +16 -0
  7. curious_pyapi-0.1.2/src/curious_pyapi/api/curious.py +335 -0
  8. curious_pyapi-0.1.2/src/curious_pyapi/exceptions/__init__.py +5 -0
  9. curious_pyapi-0.1.2/src/curious_pyapi/exceptions/exceptions.py +66 -0
  10. curious_pyapi-0.1.2/src/curious_pyapi/schema/curious.py +108 -0
  11. curious_pyapi-0.1.2/src/curious_pyapi/schema/pyapi.py +116 -0
  12. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/utils/data.py +3 -1
  13. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/utils/defaults.py +8 -2
  14. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/uv.lock +36 -36
  15. curious_pyapi-0.1.0/src/curious_pyapi/api/curious.py +0 -81
  16. curious_pyapi-0.1.0/src/curious_pyapi/schema/curious.py +0 -68
  17. curious_pyapi-0.1.0/src/curious_pyapi/schema/pyapi.py +0 -29
  18. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/.github/dependabot.yaml +0 -0
  19. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/.gitignore +0 -0
  20. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/.pre-commit-config.yaml +0 -0
  21. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/.python-version +0 -0
  22. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/LICENSE +0 -0
  23. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/README.md +0 -0
  24. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/__init__.py +0 -0
  25. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/api/__init__.py +0 -0
  26. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/py.typed +0 -0
  27. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/schema/__init__.py +0 -0
  28. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/schema/regex.py +0 -0
  29. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/utils/__init__.py +0 -0
  30. {curious_pyapi-0.1.0 → curious_pyapi-0.1.2}/src/curious_pyapi/utils/logging.py +0 -0
@@ -16,17 +16,17 @@ jobs:
16
16
 
17
17
  steps:
18
18
  - name: Checkout repository
19
- uses: actions/checkout@v4
19
+ uses: actions/checkout@v7
20
20
  with:
21
21
  fetch-depth: 0 # Full git history required for uv-dynamic-versioning
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v5
24
+ uses: astral-sh/setup-uv@v7
25
25
  with:
26
26
  enable-cache: true
27
27
 
28
28
  - name: Set up Python 3.14
29
- uses: actions/setup-python@v5
29
+ uses: actions/setup-python@v7
30
30
  with:
31
31
  python-version: "3.14"
32
32
 
@@ -0,0 +1,5 @@
1
+ # line length
2
+ MD013: false
3
+ # no duplicate headers
4
+ MD024:
5
+ siblings_only: true
@@ -0,0 +1,9 @@
1
+ {
2
+ "python-envs.pythonProjects": [
3
+ {
4
+ "path": ".",
5
+ "envManager": "ms-python.python:venv",
6
+ "packageManager": "ms-python.python:pip"
7
+ }
8
+ ]
9
+ }
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.2]
9
+
10
+ ### Added
11
+
12
+ - Badges to README.
13
+
14
+ ## [0.1.1]
15
+
16
+ ### Added
17
+
18
+ - Push account to Curious method.
19
+ - Create new Curious user method.
20
+ - Accept Curious invitation method.
21
+
8
22
  ## [0.1.0]
9
23
 
10
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: curious-pyapi
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python interface for Curious API.
5
5
  Author-email: Jon Cluce <jon.clucas@childmind.org>
6
6
  License-File: LICENSE
@@ -20,3 +20,19 @@ dependencies = [
20
20
 
21
21
  [tool.hatch.version]
22
22
  source = "uv-dynamic-versioning"
23
+
24
+ [tool.pyright]
25
+ reportUnknownMemberType = false
26
+
27
+ [tool.ruff]
28
+ line-length = 88
29
+ target-version = "py314"
30
+
31
+ [tool.ruff.lint]
32
+ extend-select = ["A", "B904", "C4", "D", "E", "EM", "F541", "G", "I", "ICN", "LOG007", "N", "NPY", "PL", "RET", "RSE", "RUF", "Q", "T20", "TRY002", "TRY201", "TRY400", "TRY401", "UP032", "W"]
33
+ ignore = ["D203", "D212", "RUF002", "RUF003"]
34
+
35
+ [tool.ruff.lint.isort]
36
+ combine-as-imports = true
37
+ force-sort-within-sections = true
38
+ order-by-type = false
@@ -0,0 +1,335 @@
1
+ """Curious API functionality."""
2
+
3
+ import json
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+ import polars as pl
8
+ from pydantic import AnyHttpUrl, SecretStr
9
+
10
+ from ..exceptions import ApiStatusError, AuthenticationError, CuriousApiError
11
+ from ..schema.curious import CuriousId, NewUserData, Record
12
+ from ..schema.pyapi import ApiClient, CuriousAuth, EndpointPath, Tokens, url
13
+ from ..utils.data import api_data
14
+ from ..utils.defaults import CURIOUS_BASE_URL, headers
15
+ from ..utils.logging import get_logger
16
+
17
+ LOGGER = get_logger(__name__)
18
+
19
+
20
+ class CuriousApiClient(ApiClient):
21
+ """API Client managing authentication, connection pooling, and endpoint routing."""
22
+
23
+ # Declarative Endpoints
24
+ applets = EndpointPath("/applets")
25
+ applet_by_id = EndpointPath("/applets/{applet_id}")
26
+ auth_login = EndpointPath("/auth/login")
27
+ invite_user = EndpointPath("/invitations/{applet_id}/{user_type}")
28
+ invitations = EndpointPath("/invitations")
29
+ invitation_accept = EndpointPath("/invitations/{key}/accept")
30
+
31
+ me = EndpointPath("/users/me")
32
+ user_create = EndpointPath("/users")
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ auth: Optional[CuriousAuth] = None,
38
+ base_url: httpx.URL | AnyHttpUrl = CURIOUS_BASE_URL,
39
+ ) -> None:
40
+ """Initialize Curious API Client."""
41
+ self.base_url = httpx.URL(str(base_url))
42
+ self._tokens: Optional[Tokens] = None
43
+ self._auth_header: Optional[str] = None
44
+ self._my_name: Optional[str] = None
45
+ self._applets: dict[str, Applet] = {}
46
+
47
+ # Always initialize a valid client instance
48
+ self._client = httpx.Client(
49
+ base_url=str(self.base_url),
50
+ follow_redirects=True,
51
+ event_hooks={
52
+ "request": [self._inject_auth_header],
53
+ "build": [self._inject_auth_header],
54
+ },
55
+ )
56
+
57
+ if auth:
58
+ self.authenticate(auth)
59
+ # Pre-load applets only after successful authentication
60
+ self.get_applets(refresh=True)
61
+
62
+ def __str__(self) -> str:
63
+ """Return string representation of Curious API client without side-effects."""
64
+ name_str = self._my_name if self._my_name else "Unauthenticated"
65
+ return f"CuriousApiClient({name_str})"
66
+
67
+ def __repr__(self) -> str:
68
+ """Return reproducible string without triggering network calls."""
69
+ name_str = f'"{self._my_name}"' if self._my_name else "None"
70
+ return f'CuriousApiClient(name={name_str}, base_url="{self.base_url}")'
71
+
72
+ def fetch_name(self) -> str:
73
+ """Fetch and return the user's name from the API."""
74
+ me = self.me.get()
75
+ me_json = me.json().get("result", {})
76
+
77
+ first = me_json.get("firstName", "")
78
+ last = me_json.get("lastName", "")
79
+ self._my_name = " ".join(filter(None, [first, last]))
80
+ return self._my_name
81
+
82
+ @property
83
+ def my_name(self) -> str:
84
+ """Return memoized string name, fetching only if authenticated and missing."""
85
+ if self._my_name is None:
86
+ if self.is_authenticated:
87
+ self.fetch_name()
88
+ else:
89
+ return "Unauthenticated"
90
+ assert isinstance(self._my_name, str)
91
+ return self._my_name
92
+
93
+ def _inject_auth_header(self, request: httpx.Request) -> None:
94
+ """Event hook executed right before any request is sent over the wire."""
95
+ if self._auth_header:
96
+ request.headers["Authorization"] = self._auth_header
97
+
98
+ def get_applets(self, refresh: bool = False) -> None:
99
+ """Gather bound Applets into a dict."""
100
+ if not refresh and self._applets:
101
+ return
102
+
103
+ applets: "dict[str, Applet]" = {}
104
+ result = self.applets.get().json().get("result", [])
105
+ for applet in result:
106
+ if {"displayName", "id"}.issubset(applet):
107
+ applets[applet["displayName"]] = Applet(
108
+ self, applet["id"], display_name=applet["displayName"]
109
+ )
110
+ self._applets = applets
111
+
112
+ @property
113
+ def is_authenticated(self) -> bool:
114
+ """Return True if the client is authenticated."""
115
+ return self._auth_header is not None
116
+
117
+ def authenticate(self, auth: CuriousAuth) -> None:
118
+ """Authenticate immediately and store Bearer token."""
119
+ tokens = self._fetch_tokens(auth.login_credentials)
120
+ if not tokens.access:
121
+ msg = "Authentication succeeded but accessToken was empty."
122
+ raise AuthenticationError(msg)
123
+
124
+ self._tokens = tokens
125
+ self._auth_header = f"Bearer {tokens.access.get_secret_value()}"
126
+ LOGGER.info("Successfully authenticated as Bearer token.")
127
+
128
+ def _fetch_tokens(self, credentials: dict[str, str | SecretStr]) -> Tokens:
129
+ """Post login credentials to obtain tokens."""
130
+ target_url = str(self.base_url.join(httpx.URL("auth/login")))
131
+ try:
132
+ response = httpx.post(
133
+ target_url,
134
+ json=api_data(credentials),
135
+ headers=headers(),
136
+ )
137
+
138
+ if httpx.codes.is_success(response.status_code):
139
+ data = response.json()
140
+ token_data = data.get("result", {}).get("token", {})
141
+ access = token_data.get("accessToken")
142
+ refresh = token_data.get("refreshToken")
143
+
144
+ if not access:
145
+ msg = f"Auth response missing 'accessToken': {data}"
146
+ raise AuthenticationError(msg)
147
+
148
+ return Tokens(access=access, refresh=refresh)
149
+
150
+ msg = f"Login failed with status {response.status_code}: {response.text}"
151
+ raise AuthenticationError(msg)
152
+
153
+ except (httpx.HTTPError, json.JSONDecodeError) as e:
154
+ msg = f"Authentication request failed: {e}"
155
+ raise AuthenticationError(msg) from e
156
+
157
+ def request(
158
+ self, method: str, path: str | httpx.URL, **kwargs: Any
159
+ ) -> httpx.Response:
160
+ """Centralized transport gateway with error mapping and auth checking."""
161
+ clean_path = str(path).lstrip("/")
162
+
163
+ if not self.is_authenticated:
164
+ LOGGER.warning(
165
+ "Attempting unauthenticated '%s' request to '%s'", method, clean_path
166
+ )
167
+ request = self._client.build_request(method, clean_path, **kwargs)
168
+ LOGGER.exception(request)
169
+ try:
170
+ response = self._client.request(method, clean_path, **kwargs)
171
+ response.raise_for_status()
172
+ return response
173
+ except httpx.HTTPStatusError as e:
174
+ LOGGER.exception("Sent Headers: %s", dict(e.request.headers))
175
+ LOGGER.exception("Response Error Details: %s", e.response.text)
176
+ raise ApiStatusError(
177
+ str(e.response), request=e.request, response=e.response
178
+ ) from e
179
+ except httpx.HTTPError as e:
180
+ msg = f"Network transport error: {e}"
181
+ raise CuriousApiError(msg) from e
182
+
183
+ def close(self) -> None:
184
+ """Close internal HTTP connection pool."""
185
+ self._client.close()
186
+
187
+ def create_accounts_from_df(
188
+ self,
189
+ df: pl.DataFrame,
190
+ applet_id: CuriousId,
191
+ user_type: str = "respondent",
192
+ *,
193
+ limit=None,
194
+ ) -> None:
195
+ """For each row in DataFrame, create an account in Curious."""
196
+ _create_accounts_from_df(self, df, applet_id, user_type, limit=limit)
197
+
198
+ def __enter__(self) -> CuriousApiClient:
199
+ """Enter context manager and return self."""
200
+ return self
201
+
202
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
203
+ """Exit context manager and close the client."""
204
+ self.close()
205
+
206
+
207
+ class Applet:
208
+ """Applet bound to a client."""
209
+
210
+ def __init__(
211
+ self,
212
+ client: CuriousApiClient,
213
+ applet_id: CuriousId,
214
+ *,
215
+ display_name: Optional[str] = None,
216
+ ) -> None:
217
+ """Initialize Applet."""
218
+ self._id = applet_id
219
+ self._client = client
220
+ if display_name:
221
+ self._name = display_name
222
+ else:
223
+ self._name = (
224
+ self._client.applet_by_id(applet_id=applet_id)
225
+ .get()
226
+ .json()["result"]["displayName"]
227
+ )
228
+
229
+ def __str__(self) -> str:
230
+ """Return string representation of Applet."""
231
+ return f"Applet({self._name})"
232
+
233
+ def __repr__(self) -> str:
234
+ """Return reproducible string representation of Applet."""
235
+ return (
236
+ f'Applet(client={self._client}, applet_id="{self._id}", '
237
+ f'display_name="{self._name}")'
238
+ )
239
+
240
+
241
+ def create_toplevel_user(new_user_data: NewUserData) -> None:
242
+ """Create a top-level user in Curious."""
243
+ client = CuriousApiClient()
244
+ response = client.user_create.post(json=new_user_data)
245
+ if not httpx.codes.is_success(response.status_code):
246
+ msg = f"Failed to create user: {response.status_code} - {response.text}"
247
+ raise ApiStatusError(
248
+ msg,
249
+ request=response.request,
250
+ response=response,
251
+ )
252
+
253
+
254
+ def get_curious_token(
255
+ curious_data: Record,
256
+ curious_headers: Optional[Record] = None,
257
+ curious_url: httpx.URL | AnyHttpUrl = CURIOUS_BASE_URL,
258
+ ) -> Optional[Tokens]:
259
+ """
260
+ Process the response to a POST request to the specified URL.
261
+
262
+ Parameters
263
+ ----------
264
+ curious_data
265
+ The data to include in the POST request.
266
+ curious_headers
267
+ The headers to include in the POST request.
268
+ curious_url
269
+ The URL to send the POST request to.
270
+
271
+ Returns
272
+ -------
273
+ Tokens or None
274
+ An object containing (access, refresh) token strings, or None if unsuccessful.
275
+
276
+ Raises
277
+ ------
278
+ RuntimeError
279
+ If there is an error with the request or response.
280
+
281
+ """
282
+ curious_headers = headers(headers=curious_headers)
283
+ curious_url = url(curious_url)
284
+ try:
285
+ # Sending the POST request
286
+ response = httpx.post(
287
+ curious_url.join("/auth/login"),
288
+ json=api_data(curious_data),
289
+ headers=curious_headers,
290
+ )
291
+ if httpx.codes.is_success(response.status_code):
292
+ response_data = response.json()
293
+ access_token = (
294
+ response_data.get("result", {})
295
+ .get("token", {})
296
+ .get("accessToken", None)
297
+ ) # Get access token
298
+ refresh_token = (
299
+ response_data.get("result", {})
300
+ .get("token", {})
301
+ .get("refreshToken", None)
302
+ ) # Get refresh token
303
+ else:
304
+ LOGGER.exception(
305
+ "Failed to fetch data: %d - %s", response.status_code, response.text
306
+ )
307
+ response.raise_for_status()
308
+ return None
309
+
310
+ return Tokens(access=access_token, refresh=refresh_token)
311
+
312
+ except httpx.HTTPError as e:
313
+ msg = f"Error sending request: {e}"
314
+ raise RuntimeError(msg) from e
315
+ except json.JSONDecodeError as e:
316
+ msg = f"Error: Response is not valid JSON! {e}"
317
+ raise RuntimeError(msg) from e
318
+
319
+
320
+ def create_accounts_from_df(
321
+ client: CuriousApiClient,
322
+ df: pl.DataFrame,
323
+ applet_id: CuriousId,
324
+ user_type: str = "respondent",
325
+ *,
326
+ limit=None,
327
+ ) -> None:
328
+ """For each row in DataFrame, create an account in Curious."""
329
+ if limit:
330
+ df = df.head(limit)
331
+ for record in df.to_dicts():
332
+ client.invite_user(applet_id=applet_id, user_type=user_type).post(json=record)
333
+
334
+
335
+ _create_accounts_from_df = create_accounts_from_df
@@ -0,0 +1,5 @@
1
+ """Exceptions."""
2
+
3
+ from .exceptions import ApiStatusError, AuthenticationError, CuriousApiError
4
+
5
+ __all__ = ["ApiStatusError", "AuthenticationError", "CuriousApiError"]
@@ -0,0 +1,66 @@
1
+ """Custom exceptions and exception handling."""
2
+
3
+ import json
4
+ from typing import Any, Callable
5
+
6
+ from httpx import codes, HTTPStatusError, RequestError, Response
7
+
8
+
9
+ def allow_existing(
10
+ post: Callable[[], Response], *, warn_if_existing: bool = True, **kwargs: Any
11
+ ) -> Response | None:
12
+ """Post, but allow existing."""
13
+ try:
14
+ response = post(**kwargs)
15
+ except Exception as e:
16
+ if hasattr(e, "response"):
17
+ _response = getattr(e, "response")
18
+ if getattr(_response, "status_code") == codes.BAD_REQUEST:
19
+ results = getattr(_response, "json")().get("result", [])
20
+ if results:
21
+ if isinstance(results[0], dict):
22
+ _request = getattr(e, "request")
23
+ warning: tuple[str, ...]
24
+ message = results[0].get("message")
25
+ match message:
26
+ case (
27
+ "That email address is already associated "
28
+ "with a Curious account."
29
+ ):
30
+ warning = (
31
+ "%s is already associated with a Curious account.",
32
+ str(
33
+ json.loads(
34
+ getattr(_request, "content", json.dumps({}))
35
+ ).get("email", "That email address")
36
+ ),
37
+ )
38
+ case "Non-unique value.":
39
+ warning = (
40
+ (
41
+ "A record with the same unique "
42
+ "value already exists."
43
+ ),
44
+ )
45
+ case _:
46
+ raise
47
+ if warn_if_existing:
48
+ from ..utils.logging import get_logger # noqa: PLC0415
49
+
50
+ logger = get_logger(__name__)
51
+ logger.warning(*warning)
52
+ return None
53
+ raise
54
+ return response
55
+
56
+
57
+ class ApiStatusError(HTTPStatusError):
58
+ """Raised when an API request returns a non-successful status code."""
59
+
60
+
61
+ class AuthenticationError(RequestError):
62
+ """Raised when authentication fails."""
63
+
64
+
65
+ class CuriousApiError(RequestError):
66
+ """Raised for general Curious API errors."""
@@ -0,0 +1,108 @@
1
+ """Curious schemas."""
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Annotated
5
+
6
+ import polars as pl
7
+ from pydantic.types import StringConstraints
8
+
9
+ from ..utils.logging import get_logger
10
+ from .regex import EMAIL_REGEX
11
+
12
+ LOGGER = get_logger(__name__)
13
+
14
+
15
+ type IntStr = int | str
16
+ """Integer or integers string."""
17
+ type IntStrContainer = list[IntStr] | set[IntStr] | tuple[IntStr, ...]
18
+ """Iterable container of integers and/or integers strings."""
19
+ type Record = (
20
+ dict[str, IntStr | IntStrContainer] | Mapping[str, IntStr | IntStrContainer]
21
+ )
22
+ """Dictionary/mapping with string keys and IntStr or IntStrContainer values."""
23
+
24
+
25
+ def _validate(
26
+ df: pl.DataFrame, schema: pl.Schema, required_cols: list[str]
27
+ ) -> pl.DataFrame:
28
+ """Validate DataFrame, adding missing columns with nulls and casting types."""
29
+ exprs = [
30
+ pl.col(col).cast(dtype)
31
+ if col in df.columns
32
+ else pl.lit(None, dtype=dtype).alias(col)
33
+ for col, dtype in schema.items()
34
+ ]
35
+
36
+ out_df = df.select(exprs)
37
+
38
+ # Evaluate against out_df where 'email' is guaranteed to exist
39
+ is_valid_email = pl.col("email").str.contains(EMAIL_REGEX).fill_null(False)
40
+
41
+ failures_df = out_df.filter(~is_valid_email)
42
+ out_df = out_df.filter(is_valid_email)
43
+
44
+ if not failures_df.is_empty():
45
+ log_cols = [col for col in required_cols if col in failures_df.columns]
46
+ LOGGER.error(
47
+ "Dropped %d records due to invalid email addresses:\n%s",
48
+ failures_df.height,
49
+ failures_df.select(log_cols),
50
+ )
51
+
52
+ return out_df
53
+
54
+
55
+ @pl.api.register_dataframe_namespace("curious_account")
56
+ class CuriousAccount:
57
+ """Curious account invitation."""
58
+
59
+ SCHEMA = pl.Schema(
60
+ {
61
+ "email": pl.String,
62
+ "firstName": pl.String,
63
+ "lastName": pl.String,
64
+ "language": pl.String,
65
+ "secretUserId": pl.String,
66
+ "nickname": pl.String,
67
+ "tag": pl.String,
68
+ }
69
+ )
70
+
71
+ def __init__(self, df: pl.DataFrame):
72
+ """Initialize CuriousAccount."""
73
+ self._df = df
74
+
75
+ def enforce_schema(self) -> pl.DataFrame:
76
+ """Enforces schema, adds missing columns with nulls, and casts types."""
77
+ return _validate(self._df, self.SCHEMA, ["email", "secretUserId", "nickname"])
78
+
79
+
80
+ CuriousId = Annotated[
81
+ str,
82
+ StringConstraints(pattern=r"^[a-zA-Z0-9]{8}(-[a-zA-Z0-9]{4}){3}-[a-zA-Z0-9]{12}$"),
83
+ ]
84
+ """ID string for a Curious entity."""
85
+
86
+
87
+ @pl.api.register_dataframe_namespace("new_curious_user")
88
+ class NewUserData:
89
+ """Required data for creating a new Curious user account."""
90
+
91
+ SCHEMA = pl.Schema(
92
+ {
93
+ "email": pl.String,
94
+ "firstName": pl.String,
95
+ "lastName": pl.String,
96
+ "password": pl.String,
97
+ }
98
+ )
99
+
100
+ def __init__(self, df: pl.DataFrame):
101
+ """Initialize NewUserData."""
102
+ self._df = df
103
+
104
+ def enforce_schema(self) -> pl.DataFrame:
105
+ """Enforces schema, adds missing columns with nulls, and casts types."""
106
+ return _validate(
107
+ self._df, self.SCHEMA, ["email", "firstName", "lastName", "password"]
108
+ )
@@ -0,0 +1,116 @@
1
+ """Curious-PyAPI schemas."""
2
+
3
+ from typing import Any, Optional, overload
4
+
5
+ from httpx import Response, URL
6
+ from pydantic import AnyHttpUrl, BaseModel, EmailStr, SecretStr
7
+
8
+ from ..utils.logging import get_logger
9
+ from .curious import CuriousId
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ class CuriousAuth(BaseModel):
15
+ """Required data for authentication and decryption."""
16
+
17
+ curious_email: EmailStr
18
+ curious_password: SecretStr
19
+ applet_id: Optional[CuriousId] = None
20
+ applet_password: Optional[SecretStr] = None
21
+
22
+ @property
23
+ def login_credentials(self) -> dict[str, str | SecretStr]:
24
+ """Return email and password as dictionary for API request."""
25
+ return {"email": self.curious_email, "password": self.curious_password}
26
+
27
+
28
+ class ApiClient:
29
+ """Generic API client."""
30
+
31
+ base_url: URL
32
+
33
+ def request(self, method: str, path: str | URL, **kwargs: Any) -> Response:
34
+ """Centralized transport gateway with error mapping and auth checking."""
35
+ return NotImplemented()
36
+
37
+
38
+ class BoundEndpoint:
39
+ """Class to bind endpoint to an instance."""
40
+
41
+ def __init__(self, client: ApiClient, path: str) -> None:
42
+ """Initialize bound endpoint."""
43
+ self._client = client
44
+ self.path = path
45
+
46
+ def __call__(self, **kwargs: Any) -> "BoundEndpoint":
47
+ """Allow calling endpoint as a function to format path parameters."""
48
+ return BoundEndpoint(self._client, self.path.format(**kwargs))
49
+
50
+ @property
51
+ def url(self) -> URL:
52
+ """Return base URL."""
53
+ return self._client.base_url.join(URL(self.path.lstrip("/")))
54
+
55
+ def get(self, **kwargs: Any) -> Response:
56
+ """Return GET response."""
57
+ return self._client.request("GET", self.path, **kwargs)
58
+
59
+ def post(self, **kwargs: Any) -> Response:
60
+ """Return POST response."""
61
+ return self._client.request("POST", self.path, **kwargs)
62
+
63
+
64
+ class EndpointPath:
65
+ """Endpoint path or full URL."""
66
+
67
+ def __init__(self, path: str) -> None:
68
+ """Initialize endpoint path."""
69
+ self.path = path
70
+
71
+ @overload
72
+ def __get__(self, instance: None, owner: Any) -> str: ...
73
+
74
+ @overload
75
+ def __get__(self, instance: ApiClient, owner: Any) -> BoundEndpoint: ...
76
+
77
+ def __get__(self, instance: Optional[ApiClient], owner: Any) -> str | BoundEndpoint:
78
+ """Return path or bound endpoint."""
79
+ if instance is None:
80
+ return self.path
81
+ return BoundEndpoint(instance, self.path)
82
+
83
+
84
+ class Endpoint:
85
+ """API endpoint."""
86
+
87
+ auth = EndpointPath("/auth/login")
88
+
89
+ def __init__(self, base_url: URL | AnyHttpUrl) -> None:
90
+ """Initialize API endpoint."""
91
+ self._base_url = url(base_url)
92
+
93
+ @property
94
+ def base_url(self) -> URL:
95
+ """Return base URL for API endpoint."""
96
+ return self._base_url
97
+
98
+ @base_url.setter
99
+ def base_url(self, value: URL) -> None:
100
+ """Update base URL for API endpoint."""
101
+ self._base_url = value
102
+
103
+
104
+ class Tokens(BaseModel):
105
+ """Curious tokens."""
106
+
107
+ access: SecretStr
108
+ refresh: SecretStr
109
+
110
+
111
+ def url(url: URL | AnyHttpUrl) -> URL:
112
+ """Typecheck and cast to `httpx.URL`."""
113
+ if isinstance(url, str):
114
+ url = URL(url)
115
+ assert isinstance(url, URL)
116
+ return url
@@ -2,8 +2,10 @@
2
2
 
3
3
  from pydantic import SecretStr
4
4
 
5
+ from ..schema.curious import Record
5
6
 
6
- def api_data(data: dict) -> dict:
7
+
8
+ def api_data(data: Record | dict[str, str | SecretStr]) -> Record:
7
9
  """Prep data for API calls."""
8
10
  return {
9
11
  k: v.get_secret_value() if isinstance(v, SecretStr) else v
@@ -4,13 +4,19 @@ from typing import Optional
4
4
 
5
5
  from httpx import URL
6
6
 
7
+ from ..schema.curious import Record
8
+
7
9
  CURIOUS_BASE_URL = URL("https://api-v2.gettingcurious.com")
8
10
  """Canonical URL for Curious API."""
9
11
 
10
12
 
11
13
  def headers(
12
- token: Optional[str] = None, headers: Optional[dict] = None
14
+ token: Optional[str] = None, headers: Optional[Record] = None
13
15
  ) -> dict[str, str]:
14
16
  """Return Curious headers."""
15
17
  auth = {"Authorization": f"Bearer {token}"} if token else {}
16
- return {"Content-Type": "application/json"} | auth | (headers or {})
18
+ return (
19
+ {"Content-Type": "application/json"}
20
+ | auth
21
+ | ({k: str(v) for k, v in headers.items()} if headers else {})
22
+ )
@@ -171,7 +171,7 @@ wheels = [
171
171
 
172
172
  [[package]]
173
173
  name = "pydantic"
174
- version = "2.13.4"
174
+ version = "2.13.5"
175
175
  source = { registry = "https://pypi.org/simple" }
176
176
  dependencies = [
177
177
  { name = "annotated-types" },
@@ -179,9 +179,9 @@ dependencies = [
179
179
  { name = "typing-extensions" },
180
180
  { name = "typing-inspection" },
181
181
  ]
182
- sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
182
+ sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" }
183
183
  wheels = [
184
- { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
184
+ { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" },
185
185
  ]
186
186
 
187
187
  [package.optional-dependencies]
@@ -191,43 +191,43 @@ email = [
191
191
 
192
192
  [[package]]
193
193
  name = "pydantic-core"
194
- version = "2.46.4"
194
+ version = "2.46.5"
195
195
  source = { registry = "https://pypi.org/simple" }
196
196
  dependencies = [
197
197
  { name = "typing-extensions" },
198
198
  ]
199
- sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
200
- wheels = [
201
- { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
202
- { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
203
- { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
204
- { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
205
- { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
206
- { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
207
- { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
208
- { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
209
- { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
210
- { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
211
- { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
212
- { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
213
- { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
214
- { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
215
- { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
216
- { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
217
- { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
218
- { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
219
- { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
220
- { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
221
- { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
222
- { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
223
- { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
224
- { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
225
- { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
226
- { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
227
- { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
228
- { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
229
- { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
230
- { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
199
+ sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" }
200
+ wheels = [
201
+ { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" },
202
+ { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" },
203
+ { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" },
204
+ { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" },
205
+ { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" },
206
+ { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" },
207
+ { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" },
208
+ { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" },
209
+ { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" },
210
+ { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" },
211
+ { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" },
212
+ { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" },
213
+ { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" },
214
+ { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" },
215
+ { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" },
216
+ { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" },
217
+ { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" },
218
+ { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" },
219
+ { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" },
220
+ { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" },
221
+ { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" },
222
+ { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" },
223
+ { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" },
224
+ { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" },
225
+ { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" },
226
+ { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" },
227
+ { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" },
228
+ { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" },
229
+ { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" },
230
+ { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" },
231
231
  ]
232
232
 
233
233
  [[package]]
@@ -1,81 +0,0 @@
1
- """Curious API functionality."""
2
-
3
- import json
4
- from typing import Optional
5
- import httpx
6
-
7
- from pydantic import AnyHttpUrl
8
-
9
- from ..schema.pyapi import Tokens
10
- from ..utils.defaults import CURIOUS_BASE_URL, headers
11
- from ..utils.data import api_data
12
- from ..utils.logging import get_logger
13
-
14
- LOGGER = get_logger(__name__)
15
-
16
-
17
- def get_curious_token(
18
- curious_data: dict,
19
- curious_headers: Optional[dict] = None,
20
- curious_url: httpx.URL | AnyHttpUrl = CURIOUS_BASE_URL,
21
- ) -> Optional[Tokens]:
22
- """Process the response to a POST request to the specified URL.
23
-
24
- Parameters
25
- ----------
26
- curious_data
27
- The data to include in the POST request.
28
- curious_headers
29
- The headers to include in the POST request.
30
- curious_url
31
- The URL to send the POST request to.
32
-
33
- Returns
34
- -------
35
- Tokens or None
36
- An object containing (access, refresh) token strings, or None if unsuccessful.
37
-
38
- Raises
39
- ------
40
- RuntimeError
41
- If there is an error with the request or response.
42
-
43
- """
44
- curious_headers = headers(headers=curious_headers)
45
- if isinstance(curious_url, str):
46
- curious_url = httpx.URL(curious_url)
47
- assert isinstance(curious_url, httpx.URL)
48
- try:
49
- # Sending the POST request
50
- response = httpx.post(
51
- curious_url.join("/auth/login"),
52
- json=api_data(curious_data),
53
- headers=curious_headers,
54
- )
55
- if response.status_code == httpx.codes.OK:
56
- response_data = response.json() # Convert response to JSON
57
- access_token = (
58
- response_data.get("result", {})
59
- .get("token", {})
60
- .get("accessToken", None)
61
- ) # Get access token
62
- refresh_token = (
63
- response_data.get("result", {})
64
- .get("token", {})
65
- .get("refreshToken", None)
66
- ) # Get refresh token
67
- else:
68
- LOGGER.exception(
69
- "Failed to fetch data: %d - %s", response.status_code, response.text
70
- )
71
- response.raise_for_status()
72
- return None
73
-
74
- return Tokens(access=access_token, refresh=refresh_token)
75
-
76
- except httpx.HTTPError as e:
77
- msg = f"Error sending request: {e}"
78
- raise RuntimeError(msg) from e
79
- except json.JSONDecodeError as e:
80
- msg = f"Error: Response is not valid JSON! {e}"
81
- raise RuntimeError(msg) from e
@@ -1,68 +0,0 @@
1
- """Curious schemas."""
2
-
3
- from typing import Annotated
4
-
5
- import polars as pl
6
- from pydantic.types import StringConstraints
7
-
8
- from .regex import EMAIL_REGEX
9
- from ..utils.logging import get_logger
10
-
11
- LOGGER = get_logger(__name__)
12
-
13
-
14
- @pl.api.register_dataframe_namespace("curious_pyapi")
15
- class CuriousAccount:
16
- SCHEMA = pl.Schema(
17
- {
18
- "email": pl.String,
19
- "firstName": pl.String,
20
- "lastName": pl.String,
21
- "language": pl.String,
22
- "secretUserId": pl.String,
23
- "nickname": pl.String,
24
- "tag": pl.String,
25
- }
26
- )
27
-
28
- def __init__(self, df: pl.DataFrame):
29
- self._df = df
30
-
31
-
32
- def enforce_schema(self) -> pl.DataFrame:
33
- """Enforces schema, adds missing columns with nulls, and casts types."""
34
- exprs = [
35
- pl.col(col).cast(dtype)
36
- if col in self._df.columns
37
- else pl.lit(None, dtype=dtype).alias(col)
38
- for col, dtype in self.SCHEMA.items()
39
- ]
40
-
41
- out_df = self._df.select(exprs)
42
-
43
- # Evaluate against out_df where 'email' is guaranteed to exist
44
- is_valid_email = pl.col("email").str.contains(EMAIL_REGEX).fill_null(False)
45
-
46
- failures_df = out_df.filter(~is_valid_email)
47
- out_df = out_df.filter(is_valid_email)
48
-
49
- if not failures_df.is_empty():
50
- log_cols = [
51
- col
52
- for col in ["email", "secretUserId", "nickname"]
53
- if col in failures_df.columns
54
- ]
55
- LOGGER.error(
56
- "Dropped %d records due to invalid email addresses:\n%s",
57
- failures_df.height,
58
- failures_df.select(log_cols),
59
- )
60
-
61
- return out_df
62
-
63
-
64
- CuriousId = Annotated[
65
- str,
66
- StringConstraints(pattern=r"^[a-zA-Z0-9]{8}(-[a-zA-Z0-9]{4}){3}-[a-zA-Z0-9]{12}$"),
67
- ]
68
- """ID string for a Curious entity."""
@@ -1,29 +0,0 @@
1
- """Curious-PyAPI schemas."""
2
-
3
- from pydantic import BaseModel, EmailStr, SecretStr
4
-
5
- from .curious import CuriousId
6
- from ..utils.logging import get_logger
7
-
8
- logger = get_logger(__name__)
9
-
10
-
11
- class CuriousAuth(BaseModel):
12
- """Required data for authentication and decryption."""
13
-
14
- curious_email: EmailStr
15
- curious_password: SecretStr
16
- applet_id: CuriousId
17
- applet_password: SecretStr
18
-
19
- @property
20
- def login_credentials(self) -> dict[str, str | SecretStr]:
21
- """Return email and password as dictionary for API request."""
22
- return {"email": self.curious_email, "password": self.curious_password}
23
-
24
-
25
- class Tokens(BaseModel):
26
- """Curious tokens."""
27
-
28
- access: SecretStr
29
- refresh: SecretStr
File without changes
File without changes
File without changes