udl-sdk 0.1.0a11__py3-none-any.whl → 0.1.0a13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: udl-sdk
3
- Version: 0.1.0a11
3
+ Version: 0.1.0a13
4
4
  Summary: The official Python library for the unifieddatalibrary API
5
5
  Project-URL: Homepage, https://github.com/Bluestaq/udl-python-sdk
6
6
  Project-URL: Repository, https://github.com/Bluestaq/udl-python-sdk
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.9
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
21
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
23
  Classifier: Typing :: Typed
23
24
  Requires-Python: >=3.8
@@ -29,12 +30,13 @@ Requires-Dist: sniffio
29
30
  Requires-Dist: typing-extensions<5,>=4.10
30
31
  Provides-Extra: aiohttp
31
32
  Requires-Dist: aiohttp; extra == 'aiohttp'
32
- Requires-Dist: httpx-aiohttp>=0.1.6; extra == 'aiohttp'
33
+ Requires-Dist: httpx-aiohttp>=0.1.8; extra == 'aiohttp'
33
34
  Description-Content-Type: text/markdown
34
35
 
35
36
  # Unifieddatalibrary Python API library
36
37
 
37
- [![PyPI version](https://github.com/Bluestaq/udl-python-sdk/tree/main/<https://img.shields.io/pypi/v/udl-sdk.svg?label=pypi%20(stable)>)](https://pypi.org/project/udl-sdk/)
38
+ <!-- prettier-ignore -->
39
+ [![PyPI version](https://img.shields.io/pypi/v/udl-sdk.svg?label=pypi%20(stable))](https://pypi.org/project/udl-sdk/)
38
40
 
39
41
  The Unifieddatalibrary Python library provides convenient access to the Unifieddatalibrary REST API from any Python 3.8+
40
42
  application. The library includes type definitions for all request params and response fields,
@@ -114,7 +116,6 @@ pip install --pre udl-sdk[aiohttp]
114
116
  Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
115
117
 
116
118
  ```python
117
- import os
118
119
  import asyncio
119
120
  from unifieddatalibrary import DefaultAioHttpClient
120
121
  from unifieddatalibrary import AsyncUnifieddatalibrary
@@ -122,8 +123,8 @@ from unifieddatalibrary import AsyncUnifieddatalibrary
122
123
 
123
124
  async def main() -> None:
124
125
  async with AsyncUnifieddatalibrary(
125
- username=os.environ.get("UDL_AUTH_USERNAME"), # This is the default and can be omitted
126
- password=os.environ.get("UDL_AUTH_PASSWORD"), # This is the default and can be omitted
126
+ username="My Username",
127
+ password="My Password",
127
128
  http_client=DefaultAioHttpClient(),
128
129
  ) as client:
129
130
  page = await client.elsets.current.list()
@@ -1,18 +1,18 @@
1
1
  unifieddatalibrary/__init__.py,sha256=562u3LZ5X6bc29nGRt5uXC91XbddnlSfk7tSqKA4yVk,2727
2
- unifieddatalibrary/_base_client.py,sha256=LZy7nMp9cjwI0TIUOO0mzkhuoHa4gjhj96WDINLReiM,66727
2
+ unifieddatalibrary/_base_client.py,sha256=kv98LleyLFwecp-SyYKZVR-3Dt5OYNbmB_SQLIen6Yo,67579
3
3
  unifieddatalibrary/_client.py,sha256=mrBWoR1advnC0xyt7nMU-eiLqRf8ZBFqU8xFsrP4grU,136998
4
4
  unifieddatalibrary/_compat.py,sha256=VWemUKbj6DDkQ-O4baSpHVLJafotzeXmCQGJugfVTIw,6580
5
5
  unifieddatalibrary/_constants.py,sha256=S14PFzyN9-I31wiV7SmIlL5Ga0MLHxdvegInGdXH7tM,462
6
6
  unifieddatalibrary/_exceptions.py,sha256=rkk8r4oyqb4bxjMtx0OFWsh1m8gsMfrnIGnNPO6zRz0,3244
7
7
  unifieddatalibrary/_files.py,sha256=FZ264pl2ebvWlVztvuAE8hl1yitTQIT-2Bi6GMwMReA,3619
8
- unifieddatalibrary/_models.py,sha256=G1vczEodX0vUySeVKbF-mbzlaObNL1oVAYH4c65agRk,29131
8
+ unifieddatalibrary/_models.py,sha256=viD5E6aDMhxslcFHDYvkHaKzE8YLcNmsPsMe8STixvs,29294
9
9
  unifieddatalibrary/_qs.py,sha256=AOkSz4rHtK4YI3ZU_kzea-zpwBUgEY8WniGmTPyEimc,4846
10
10
  unifieddatalibrary/_resource.py,sha256=Ik-pULzkvFIY2OgB9Ra7mIQKCle38FSP36dWWCH9vxk,1172
11
11
  unifieddatalibrary/_response.py,sha256=hdekMRMvxTvYdKfYIPvAxSpdiuRILRCYd5Dwcye-icg,28890
12
12
  unifieddatalibrary/_streaming.py,sha256=LwKrocz7ZRmYd47TA3q-PLXwgdTgjANE-TCIRZB958s,10148
13
13
  unifieddatalibrary/_types.py,sha256=mslWUKYM1Q3bMXxgq4Mr9fo3QvAwzQJrscV44EPbmGA,6209
14
- unifieddatalibrary/_version.py,sha256=E6BVaJ5lc_V2L0pH5HcTrZMelpC26PdpKUtkXqvw4wo,179
15
- unifieddatalibrary/pagination.py,sha256=Uuf4Q1bzJIgeFOadEoqFrzO8xbzmvTR6RJmO0cYM6zw,2415
14
+ unifieddatalibrary/_version.py,sha256=4xoSZ-FMWPBL5-DynK0nN9F1JSHXcetMW2_4yc6quMk,179
15
+ unifieddatalibrary/pagination.py,sha256=jRNsKPCwBHKIHIIX5V5yB7NUsvfy-k7UqnpPGrGyFNU,5758
16
16
  unifieddatalibrary/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  unifieddatalibrary/_utils/__init__.py,sha256=PNZ_QJuzZEgyYXqkO1HVhGkj5IU9bglVUcw7H-Knjzw,2062
18
18
  unifieddatalibrary/_utils/_logs.py,sha256=mZd6C1F4ajWju5tJvYN_5ATHUi2zEzhcBQqJafS7Two,810
@@ -21,14 +21,14 @@ unifieddatalibrary/_utils/_reflection.py,sha256=ZmGkIgT_PuwedyNBrrKGbxoWtkpytJNU
21
21
  unifieddatalibrary/_utils/_resources_proxy.py,sha256=IEVfHgsswOT5HDg1cmdTtY8oOs3fo74UtR1aO6yMTZU,649
22
22
  unifieddatalibrary/_utils/_streams.py,sha256=SMC90diFFecpEg_zgDRVbdR3hSEIgVVij4taD-noMLM,289
23
23
  unifieddatalibrary/_utils/_sync.py,sha256=TpGLrrhRNWTJtODNE6Fup3_k7zrWm1j2RlirzBwre-0,2862
24
- unifieddatalibrary/_utils/_transform.py,sha256=sb9J55txMzvFJv2j7eEwiksN2T5VKvKaG80V_fpONlc,15827
24
+ unifieddatalibrary/_utils/_transform.py,sha256=V2pcnNHKQvgqpShVjV1WjRtrWlCWWdIkZP_HduzypNg,15827
25
25
  unifieddatalibrary/_utils/_typing.py,sha256=D0DbbNu8GnYQTSICnTSHDGsYXj8TcAKyhejb0XcnjtY,4602
26
- unifieddatalibrary/_utils/_utils.py,sha256=PIEBlcoQMcd2YCTmSpJSTfYhH2g3FmjHCHCvtOfagm8,12334
26
+ unifieddatalibrary/_utils/_utils.py,sha256=LLsABnJOF9OiH4aE5_nRwIya7lcn0b6BxBHY1Zgs_K8,12334
27
27
  unifieddatalibrary/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
28
- unifieddatalibrary/lib/common.py,sha256=x0DdmV6AIYIBkR4crwI3LAyL9wPL3lXFpZ9321A90TA,85
29
- unifieddatalibrary/lib/model_based_query.py,sha256=RZ1l-fBUj6HgUPb7V8V8ch-tLn4DbdrtHSAn8tX-hPE,4364
30
- unifieddatalibrary/lib/query_field_names.py,sha256=6YNz47bJGV5ICJXADECGCySjvnrREnfGSV-URAJkBew,5695
31
- unifieddatalibrary/lib/util.py,sha256=A2suZHIcXpW0ogkmluIhQvTRaSZWwpRGCZPkycRRspI,602
28
+ unifieddatalibrary/lib/common.py,sha256=nKvnmTVUrTib17inEUTUcRKCk7xcVPfuRDJdjmzwHBU,83
29
+ unifieddatalibrary/lib/model_based_query.py,sha256=xU9ixbT-sTejHNgLwJE8rcQ0KZA7V9iwV_7JxBtma9g,4373
30
+ unifieddatalibrary/lib/query_field_names.py,sha256=KSLSGM6mKMqNRpEL67xq83m9jCOscsZlEzG1F1kzmx0,5699
31
+ unifieddatalibrary/lib/util.py,sha256=9RfOZDHVPIAHh7Gp64zwZY0EFrHIldEgQ5rOhWFgGMI,604
32
32
  unifieddatalibrary/resources/__init__.py,sha256=lH1bSmd-k2lILj5bvgQyopgXics8PU-XX_7N5syfaEw,86715
33
33
  unifieddatalibrary/resources/air_events.py,sha256=mTbkac1Uttbqy6DWU0m_Xo4QQbjP6VkeMRA53XeC2Kc,83645
34
34
  unifieddatalibrary/resources/air_load_plans.py,sha256=-uLt1LheEw8MYv_DUs4MtjEvlsFy6NtekEqBOqYMkRg,61618
@@ -102,7 +102,7 @@ unifieddatalibrary/resources/rf_emitter_details.py,sha256=j_aQc0CrkkBYScJGyg2LPh
102
102
  unifieddatalibrary/resources/route_stats.py,sha256=tuKsGa6TVbKeS8xWFfKqTuXwi55CzZUZepXBaJIAhVU,71061
103
103
  unifieddatalibrary/resources/scientific.py,sha256=3z3Opivmv8PRULmRHrRBnr7J1yvcaZYuLdUcDbRxuUo,48200
104
104
  unifieddatalibrary/resources/scs_views.py,sha256=Eda3GPy4XyDVPzslQiyT3glccbM3n9xRSd03B2c7qKw,7308
105
- unifieddatalibrary/resources/secure_messaging.py,sha256=iVAJCU-ILKI4qV_5So91k0jVpBy86zW6y4xUSdHb6iI,18269
105
+ unifieddatalibrary/resources/secure_messaging.py,sha256=L6Gn0JKk6mGea1AecP2B7CDPBHZYrLMMvlrO4VM0lZU,18646
106
106
  unifieddatalibrary/resources/sensor_observation_type.py,sha256=ZPc7MyrNehH8Zk__eXqf-NPYBUYLrS2jORYnrFMZveE,13935
107
107
  unifieddatalibrary/resources/sensor_type.py,sha256=6x9ASy-L4IBsceELFtbdZU2kuInXblLAAyYlz-8rTPY,13577
108
108
  unifieddatalibrary/resources/sera_data_comm_details.py,sha256=zEcbCknfEybPh-IqCHsMSfMPJRkZH5etYYYe_slusk4,65774
@@ -2610,7 +2610,7 @@ unifieddatalibrary/types/weather_report/history_count_params.py,sha256=KFXFsL7q4
2610
2610
  unifieddatalibrary/types/weather_report/history_count_response.py,sha256=ZAHTF5IoOvMAg9GyvtMvgeowh-ythtIJhcWtMbcM8wk,202
2611
2611
  unifieddatalibrary/types/weather_report/history_list_params.py,sha256=JUd53E70wjtn7HlQ7LOZ7lUJREDJkaDYHv2eYW60T3Q,1019
2612
2612
  unifieddatalibrary/types/weather_report/weather_report_full.py,sha256=iv7fHg9aQh6ztvl0ztve5Z3_8Ww-efPMBgrS0n5gXvM,20751
2613
- udl_sdk-0.1.0a11.dist-info/METADATA,sha256=ZNG-6Rl3UDQks01rGPNRlVU7c0bYMm6E1w5-SEcQZOc,17131
2614
- udl_sdk-0.1.0a11.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
2615
- udl_sdk-0.1.0a11.dist-info/licenses/LICENSE,sha256=YBMC8KbJHXtxIo1-d_G5WdWjtiwFyrcZ5jAsRwN4POI,11348
2616
- udl_sdk-0.1.0a11.dist-info/RECORD,,
2613
+ udl_sdk-0.1.0a13.dist-info/METADATA,sha256=c8rfYagi-Yyvx5QmrHo8w20z1Nf1SwowVLNvbkTf_2Y,17014
2614
+ udl_sdk-0.1.0a13.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
2615
+ udl_sdk-0.1.0a13.dist-info/licenses/LICENSE,sha256=YBMC8KbJHXtxIo1-d_G5WdWjtiwFyrcZ5jAsRwN4POI,11348
2616
+ udl_sdk-0.1.0a13.dist-info/RECORD,,
@@ -89,6 +89,7 @@ log: logging.Logger = logging.getLogger(__name__)
89
89
  # TODO: make base page type vars covariant
90
90
  SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]")
91
91
  AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]")
92
+ _BasePageT = TypeVar("_BasePageT", bound="BasePage[Any]")
92
93
 
93
94
 
94
95
  _T = TypeVar("_T")
@@ -174,6 +175,7 @@ class BasePage(GenericModel, Generic[_T]):
174
175
  next_page_info(): Get the necessary information to make a request for the next page
175
176
  """
176
177
 
178
+ _response: httpx.Response = PrivateAttr()
177
179
  _options: FinalRequestOptions = PrivateAttr()
178
180
  _model: Type[_T] = PrivateAttr()
179
181
 
@@ -222,6 +224,23 @@ class BasePage(GenericModel, Generic[_T]):
222
224
 
223
225
  raise ValueError("Unexpected PageInfo state")
224
226
 
227
+ @classmethod
228
+ def build(cls: Type[_BasePageT], *, response: httpx.Response, data: object) -> _BasePageT: # noqa: ARG003
229
+ return cls._with_response(
230
+ cls.construct(
231
+ None,
232
+ **{
233
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {}),
234
+ },
235
+ ),
236
+ response,
237
+ )
238
+
239
+ @classmethod
240
+ def _with_response(cls, inst: _BasePageT, response: httpx.Response) -> _BasePageT:
241
+ inst._response = response
242
+ return inst
243
+
225
244
 
226
245
  class BaseSyncPage(BasePage[_T], Generic[_T]):
227
246
  _client: SyncAPIClient = pydantic.PrivateAttr()
@@ -529,6 +548,15 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
529
548
  # work around https://github.com/encode/httpx/discussions/2880
530
549
  kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
531
550
 
551
+ is_body_allowed = options.method.lower() != "get"
552
+
553
+ if is_body_allowed:
554
+ kwargs["json"] = json_data if is_given(json_data) else None
555
+ kwargs["files"] = files
556
+ else:
557
+ headers.pop("Content-Type", None)
558
+ kwargs.pop("data", None)
559
+
532
560
  # TODO: report this error to httpx
533
561
  return self._client.build_request( # pyright: ignore[reportUnknownMemberType]
534
562
  headers=headers,
@@ -540,8 +568,6 @@ class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]):
540
568
  # so that passing a `TypedDict` doesn't cause an error.
541
569
  # https://github.com/microsoft/pyright/issues/3526#event-6715453066
542
570
  params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None,
543
- json=json_data if is_given(json_data) else None,
544
- files=files,
545
571
  **kwargs,
546
572
  )
547
573
 
@@ -2,9 +2,10 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import inspect
5
- from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, cast
5
+ from typing import TYPE_CHECKING, Any, Type, Union, Generic, TypeVar, Callable, Optional, cast
6
6
  from datetime import date, datetime
7
7
  from typing_extensions import (
8
+ List,
8
9
  Unpack,
9
10
  Literal,
10
11
  ClassVar,
@@ -366,7 +367,7 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
366
367
  if type_ is None:
367
368
  raise RuntimeError(f"Unexpected field type is None for {key}")
368
369
 
369
- return construct_type(value=value, type_=type_)
370
+ return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))
370
371
 
371
372
 
372
373
  def is_basemodel(type_: type) -> bool:
@@ -420,7 +421,7 @@ def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T:
420
421
  return cast(_T, construct_type(value=value, type_=type_))
421
422
 
422
423
 
423
- def construct_type(*, value: object, type_: object) -> object:
424
+ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object:
424
425
  """Loose coercion to the expected type with construction of nested values.
425
426
 
426
427
  If the given value does not match the expected type then it is returned as-is.
@@ -438,8 +439,10 @@ def construct_type(*, value: object, type_: object) -> object:
438
439
  type_ = type_.__value__ # type: ignore[unreachable]
439
440
 
440
441
  # unwrap `Annotated[T, ...]` -> `T`
441
- if is_annotated_type(type_):
442
- meta: tuple[Any, ...] = get_args(type_)[1:]
442
+ if metadata is not None:
443
+ meta: tuple[Any, ...] = tuple(metadata)
444
+ elif is_annotated_type(type_):
445
+ meta = get_args(type_)[1:]
443
446
  type_ = extract_type_arg(type_, 0)
444
447
  else:
445
448
  meta = tuple()
@@ -231,7 +231,7 @@ def _format_data(data: object, format_: PropertyFormat, format_template: str | N
231
231
  if isinstance(data, (date, datetime)):
232
232
  if format_ == "iso8601":
233
233
  if isinstance(data, datetime):
234
- return data.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
234
+ return data.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
235
235
  return data.isoformat()
236
236
 
237
237
  if format_ == "custom" and format_template is not None:
@@ -395,7 +395,7 @@ async def _async_format_data(data: object, format_: PropertyFormat, format_templ
395
395
  if isinstance(data, (date, datetime)):
396
396
  if format_ == "iso8601":
397
397
  if isinstance(data, datetime):
398
- return data.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
398
+ return data.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
399
399
  return data.isoformat()
400
400
 
401
401
  if format_ == "custom" and format_template is not None:
@@ -417,6 +417,6 @@ def json_safe(data: object) -> object:
417
417
  return [json_safe(item) for item in data]
418
418
 
419
419
  if isinstance(data, (datetime, date)):
420
- return data.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
420
+ return data.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
421
421
 
422
422
  return data
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "unifieddatalibrary"
4
- __version__ = "0.1.0-alpha.11" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.13" # x-release-please-version
@@ -1,3 +1,2 @@
1
1
  UDL_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
2
2
  KAFKA_HEADER = "KAFKA_NEXT_OFFSET"
3
-
@@ -1,10 +1,12 @@
1
1
  # Proposed approach 2 for supporting query parameters with dynamic method generation
2
2
 
3
- from typing import Type, Dict, Any, Generic, TypeVar, Callable
3
+ from typing import Any, Dict, Type, Generic, TypeVar, Callable
4
+
4
5
  from pydantic import BaseModel
5
6
 
6
7
  T = TypeVar("T", bound=BaseModel)
7
8
 
9
+
8
10
  class Query(Generic[T]):
9
11
  """
10
12
  A dynamic query builder that generates field-based filter methods at runtime.
@@ -48,7 +50,7 @@ class Query(Generic[T]):
48
50
  }
49
51
 
50
52
  # Dynamically create query methods for each field/operator combination
51
- for field_name, field_info in model.model_fields.items():
53
+ for field_name, _field_info in model.model_fields.items():
52
54
  for suffix, operator in operators.items():
53
55
  method_name = f"{field_name}{suffix}"
54
56
  method = self._make_method(field_name, operator)
@@ -65,8 +67,10 @@ class Query(Generic[T]):
65
67
  Returns:
66
68
  A callable method that accepts a value and returns the updated Query object.
67
69
  """
70
+
68
71
  def method(self: "Query[T]", value: Any) -> "Query[T]":
69
72
  return self._add_filter(field_name, operator, value)
73
+
70
74
  return method
71
75
 
72
76
  def _add_filter(self, field_name: str, operator: str, value: Any) -> "Query[T]":
@@ -1,10 +1,11 @@
1
1
  # Proposed approach 1 for supporting query parameters in a typed and composable way
2
2
 
3
- from typing import Type, Dict, Any, Generic, TypeVar, Protocol, Union, Tuple, cast
4
- from pydantic import BaseModel
3
+ from typing import Any, Dict, Type, Tuple, Union, Generic, TypeVar, Protocol, cast
5
4
  from datetime import datetime
6
- from .util import sanitize_datetime
7
5
 
6
+ from pydantic import BaseModel
7
+
8
+ from .util import sanitize_datetime
8
9
 
9
10
  T = TypeVar("T", bound=BaseModel)
10
11
 
@@ -16,6 +17,7 @@ class QueryField(Protocol):
16
17
  Each method represents a filter operation that returns a modified Query object
17
18
  with the new filter applied.
18
19
  """
20
+
19
21
  def eq(self, value: Any) -> "Query[Any]": ...
20
22
  def gte(self, value: Any) -> "Query[Any]": ...
21
23
  def lte(self, value: Any) -> "Query[Any]": ...
@@ -91,7 +93,7 @@ class Query(Generic[T]):
91
93
  if model_info is None:
92
94
  raise KeyError(f"{self.field_name} does not exist in {self.query.model}")
93
95
 
94
- key = getattr(model_info, 'alias', None) or self.field_name
96
+ key = getattr(model_info, "alias", None) or self.field_name
95
97
 
96
98
  if operator == "between":
97
99
  if not isinstance(value, tuple) or len(value) != 2:
@@ -1,7 +1,8 @@
1
-
2
1
  from datetime import datetime, timezone
2
+
3
3
  from .common import UDL_DATETIME_FORMAT
4
4
 
5
+
5
6
  def sanitize_datetime(val: datetime) -> str:
6
7
  """Takes a datetime argument val and returns the same datetime converted to UTC.
7
8
  If tzinfo is not set, assumes local time in conversion."""
@@ -1,17 +1,23 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
- from typing import Any, List, Type, Generic, Mapping, TypeVar, Optional, cast
3
+ from typing import Any, List, Type, Generic, Mapping, TypeVar, Callable, Optional, cast
4
4
  from typing_extensions import override
5
5
 
6
- from httpx import Response
6
+ from httpx import URL, Response
7
7
 
8
8
  from ._utils import is_mapping
9
9
  from ._models import BaseModel
10
10
  from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
11
11
 
12
- __all__ = ["SyncOffsetPage", "AsyncOffsetPage"]
12
+ __all__ = [
13
+ "SyncOffsetPage",
14
+ "AsyncOffsetPage",
15
+ "SyncKafkaOffsetPage",
16
+ "AsyncKafkaOffsetPage",
17
+ ]
13
18
 
14
19
  _BaseModelT = TypeVar("_BaseModelT", bound=BaseModel)
20
+ _BasePageT = TypeVar("_BasePageT", bound=BasePage[Any])
15
21
 
16
22
  _T = TypeVar("_T")
17
23
 
@@ -38,12 +44,16 @@ class SyncOffsetPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
38
44
  return PageInfo(params={"firstResult": current_count})
39
45
 
40
46
  @classmethod
41
- def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
42
- return cls.construct(
43
- None,
44
- **{
45
- **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
46
- },
47
+ @override
48
+ def build(cls: Type[_BasePageT], *, response: Response, data: object) -> _BasePageT: # noqa: ARG003
49
+ return cls._with_response(
50
+ cls.construct(
51
+ None,
52
+ **{
53
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
54
+ },
55
+ ),
56
+ response,
47
57
  )
48
58
 
49
59
 
@@ -69,10 +79,114 @@ class AsyncOffsetPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
69
79
  return PageInfo(params={"firstResult": current_count})
70
80
 
71
81
  @classmethod
72
- def build(cls: Type[_BaseModelT], *, response: Response, data: object) -> _BaseModelT: # noqa: ARG003
73
- return cls.construct(
74
- None,
75
- **{
76
- **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
77
- },
82
+ @override
83
+ def build(cls: Type[_BasePageT], *, response: Response, data: object) -> _BasePageT: # noqa: ARG003
84
+ return cls._with_response(
85
+ cls.construct(
86
+ None,
87
+ **{
88
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
89
+ },
90
+ ),
91
+ response,
92
+ )
93
+
94
+
95
+ class SyncKafkaOffsetPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
96
+ """Pagination for Kafka-style endpoints that return the next offset in a response header."""
97
+
98
+ items: List[_T]
99
+ url_builder: Callable[[int], str]
100
+
101
+ @staticmethod
102
+ def with_url_builder(fn: Callable[[int], str]) -> Type["SyncKafkaOffsetPage[object]"]:
103
+ """Create a page class with a URL builder for constructing next page URLs."""
104
+
105
+ class PageWithBuilder(SyncKafkaOffsetPage[object]):
106
+ url_builder = fn
107
+
108
+ return PageWithBuilder
109
+
110
+ @override
111
+ def _get_page_items(self) -> List[_T]:
112
+ items = self.items
113
+ if not items:
114
+ return []
115
+ return items
116
+
117
+ @override
118
+ def next_page_info(self) -> Optional[PageInfo]:
119
+ next_offset_str = self._response.headers.get("KAFKA_NEXT_OFFSET")
120
+ if not next_offset_str:
121
+ return None
122
+
123
+ try:
124
+ next_offset = int(next_offset_str)
125
+ except ValueError:
126
+ return None
127
+
128
+ new_url = self.url_builder(next_offset)
129
+ return PageInfo(url=URL(new_url))
130
+
131
+ @classmethod
132
+ @override
133
+ def build(cls: Type[_BasePageT], *, response: Response, data: object) -> _BasePageT: # noqa: ARG003
134
+ return cls._with_response(
135
+ cls.construct(
136
+ None,
137
+ **{
138
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
139
+ },
140
+ ),
141
+ response,
142
+ )
143
+
144
+
145
+ class AsyncKafkaOffsetPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
146
+ """Async pagination for Kafka-style endpoints that return the next offset in a response header."""
147
+
148
+ items: List[_T]
149
+ url_builder: Callable[[int], str]
150
+
151
+ @staticmethod
152
+ def with_url_builder(fn: Callable[[int], str]) -> Type["AsyncKafkaOffsetPage[object]"]:
153
+ """Create a page class with a URL builder for constructing next page URLs."""
154
+
155
+ class PageWithBuilder(AsyncKafkaOffsetPage[object]):
156
+ url_builder = fn
157
+
158
+ return PageWithBuilder
159
+
160
+ @override
161
+ def _get_page_items(self) -> List[_T]:
162
+ items = self.items
163
+ if not items:
164
+ return []
165
+ return items
166
+
167
+ @override
168
+ def next_page_info(self) -> Optional[PageInfo]:
169
+ next_offset_str = self._response.headers.get("KAFKA_NEXT_OFFSET")
170
+ if not next_offset_str:
171
+ return None
172
+
173
+ try:
174
+ next_offset = int(next_offset_str)
175
+ except ValueError:
176
+ return None
177
+
178
+ new_url = self.url_builder(next_offset)
179
+ return PageInfo(url=URL(new_url))
180
+
181
+ @classmethod
182
+ @override
183
+ def build(cls: Type[_BasePageT], *, response: Response, data: object) -> _BasePageT: # noqa: ARG003
184
+ return cls._with_response(
185
+ cls.construct(
186
+ None,
187
+ **{
188
+ **(cast(Mapping[str, Any], data) if is_mapping(data) else {"items": data}),
189
+ },
190
+ ),
191
+ response,
78
192
  )
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import httpx
6
6
 
7
+ from unifieddatalibrary.pagination import SyncKafkaOffsetPage, AsyncKafkaOffsetPage
8
+
7
9
  from ..types import (
8
10
  secure_messaging_get_messages_params,
9
11
  secure_messaging_describe_topic_params,
@@ -150,7 +152,7 @@ class SecureMessagingResource(SyncAPIResource):
150
152
  extra_query: Query | None = None,
151
153
  extra_body: Body | None = None,
152
154
  timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
153
- ) -> None:
155
+ ) -> SyncKafkaOffsetPage[object]:
154
156
  """Retrieve a set of messages from the given topic at the given offset.
155
157
 
156
158
  See Help >
@@ -168,8 +170,9 @@ class SecureMessagingResource(SyncAPIResource):
168
170
  if not topic:
169
171
  raise ValueError(f"Expected a non-empty value for `topic` but received {topic!r}")
170
172
  extra_headers = {"Accept": "*/*", **(extra_headers or {})}
171
- return self._get(
173
+ return self._get_api_list(
172
174
  f"/sm/getMessages/{topic}/{offset}",
175
+ page=SyncKafkaOffsetPage.with_url_builder(lambda next_offset: f"/sm/getMessages/{topic}/{next_offset}"),
173
176
  options=make_request_options(
174
177
  extra_headers=extra_headers,
175
178
  extra_query=extra_query,
@@ -183,7 +186,7 @@ class SecureMessagingResource(SyncAPIResource):
183
186
  secure_messaging_get_messages_params.SecureMessagingGetMessagesParams,
184
187
  ),
185
188
  ),
186
- cast_to=NoneType,
189
+ model=object,
187
190
  )
188
191
 
189
192
  def list_topics(
@@ -330,7 +333,7 @@ class AsyncSecureMessagingResource(AsyncAPIResource):
330
333
  extra_query: Query | None = None,
331
334
  extra_body: Body | None = None,
332
335
  timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
333
- ) -> None:
336
+ ) -> AsyncKafkaOffsetPage[object]:
334
337
  """Retrieve a set of messages from the given topic at the given offset.
335
338
 
336
339
  See Help >
@@ -348,8 +351,9 @@ class AsyncSecureMessagingResource(AsyncAPIResource):
348
351
  if not topic:
349
352
  raise ValueError(f"Expected a non-empty value for `topic` but received {topic!r}")
350
353
  extra_headers = {"Accept": "*/*", **(extra_headers or {})}
351
- return await self._get(
354
+ return await self._get_api_list(
352
355
  f"/sm/getMessages/{topic}/{offset}",
356
+ page=AsyncKafkaOffsetPage.with_url_builder(lambda next_offset: f"/sm/getMessages/{topic}/{next_offset}"),
353
357
  options=make_request_options(
354
358
  extra_headers=extra_headers,
355
359
  extra_query=extra_query,
@@ -363,7 +367,7 @@ class AsyncSecureMessagingResource(AsyncAPIResource):
363
367
  secure_messaging_get_messages_params.SecureMessagingGetMessagesParams,
364
368
  ),
365
369
  ),
366
- cast_to=NoneType,
370
+ model=object,
367
371
  )
368
372
 
369
373
  async def list_topics(