unique_toolkit 0.8.42__py3-none-any.whl → 0.8.44__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.
@@ -3,10 +3,11 @@ This module provides a minimal framework for building endpoint classes such that
3
3
  the endpoints without having to know the details of the endpoints.
4
4
  """
5
5
 
6
- from collections.abc import Callable
6
+ from enum import StrEnum
7
7
  from string import Formatter, Template
8
8
  from typing import (
9
9
  Any,
10
+ Callable,
10
11
  Generic,
11
12
  ParamSpec,
12
13
  Protocol,
@@ -15,46 +16,87 @@ from typing import (
15
16
 
16
17
  from pydantic import BaseModel
17
18
 
19
+ # Paramspecs
20
+ PayloadParamSpec = ParamSpec("PayloadParamSpec")
21
+ PathParamsSpec = ParamSpec("PathParamsSpec")
22
+
18
23
  # Type variables
19
- ResponseType = TypeVar("ResponseType", bound=BaseModel)
24
+ ResponseType = TypeVar("ResponseType", bound=BaseModel, covariant=True)
20
25
  PathParamsType = TypeVar("PathParamsType", bound=BaseModel)
21
- RequestBodyType = TypeVar("RequestBodyType", bound=BaseModel)
26
+ PayloadType = TypeVar("PayloadType", bound=BaseModel)
22
27
 
23
- # ParamSpecs for function signatures
24
- RequestConstructorSpec = ParamSpec("RequestConstructorSpec")
25
- PathParamsSpec = ParamSpec("PathParamsSpec")
26
- RequestBodySpec = ParamSpec("RequestBodySpec")
28
+ # Helper type to extract constructor parameters
29
+
30
+ # Type for the constructor of a Pydantic model
31
+ ModelConstructor = Callable[..., BaseModel]
32
+
33
+
34
+ class EndpointMethods(StrEnum):
35
+ GET = "GET"
36
+ POST = "POST"
37
+ PUT = "PUT"
38
+ DELETE = "DELETE"
39
+ PATCH = "PATCH"
40
+ OPTIONS = "OPTIONS"
41
+ HEAD = "HEAD"
27
42
 
28
43
 
29
44
  # Necessary for typing of make_endpoint_class
30
- class EndpointClassProtocol(Protocol, Generic[PathParamsSpec, RequestBodySpec]):
45
+ class EndpointClassProtocol(
46
+ Protocol,
47
+ Generic[
48
+ PathParamsSpec,
49
+ PathParamsType,
50
+ PayloadParamSpec,
51
+ PayloadType,
52
+ ResponseType,
53
+ ],
54
+ ):
55
+ path_params_model: type[PathParamsType]
56
+ payload_model: type[PayloadType]
57
+ response_model: type[ResponseType]
58
+
31
59
  @staticmethod
32
60
  def create_url(
33
61
  *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
34
62
  ) -> str: ...
35
63
 
64
+ @staticmethod
65
+ def create_url_from_model(path_params: PathParamsType) -> str: ...
66
+
36
67
  @staticmethod
37
68
  def create_payload(
38
- *args: RequestBodySpec.args, **kwargs: RequestBodySpec.kwargs
69
+ *args: PayloadParamSpec.args, **kwargs: PayloadParamSpec.kwargs
39
70
  ) -> dict[str, Any]: ...
40
71
 
72
+ @staticmethod
73
+ def create_payload_from_model(payload: PayloadType) -> dict[str, Any]: ...
41
74
 
42
- # Model for any client to implement
43
- class Client(Protocol):
44
- def request(
45
- self,
46
- endpoint: EndpointClassProtocol,
47
- ) -> dict[str, Any]: ...
75
+ @staticmethod
76
+ def handle_response(response: dict[str, Any]) -> ResponseType: ...
48
77
 
78
+ @staticmethod
79
+ def request_method() -> EndpointMethods: ...
49
80
 
81
+
82
+ # Model for any client to implement
50
83
  def build_endpoint_class(
51
84
  *,
85
+ method: EndpointMethods,
52
86
  url_template: Template,
53
- path_params_model: Callable[PathParamsSpec, PathParamsType],
54
- payload_model: Callable[RequestBodySpec, RequestBodyType],
55
- response_model: type[ResponseType],
87
+ path_params_constructor: Callable[PathParamsSpec, PathParamsType],
88
+ payload_constructor: Callable[PayloadParamSpec, PayloadType],
89
+ response_model_type: type[ResponseType],
56
90
  dump_options: dict | None = None,
57
- ) -> type[EndpointClassProtocol[PathParamsSpec, RequestBodySpec]]:
91
+ ) -> type[
92
+ EndpointClassProtocol[
93
+ PathParamsSpec,
94
+ PathParamsType,
95
+ PayloadParamSpec,
96
+ PayloadType,
97
+ ResponseType,
98
+ ]
99
+ ]:
58
100
  """Generate a class with static methods for endpoint handling.
59
101
 
60
102
  Uses separate models for path parameters and request body for clean API design.
@@ -63,6 +105,13 @@ def build_endpoint_class(
63
105
  - create_url: Creates URL from path parameters
64
106
  - create_payload: Creates request body payload
65
107
  """
108
+
109
+ # Verify that the path_params_constructor and payload_constructor are valid pydantic models
110
+ if not isinstance(path_params_constructor, type(BaseModel)):
111
+ raise ValueError("path_params_constructor must be a pydantic model")
112
+ if not isinstance(payload_constructor, type(BaseModel)):
113
+ raise ValueError("payload_constructor must be a pydantic model")
114
+
66
115
  if not dump_options:
67
116
  dump_options = {
68
117
  "exclude_unset": True,
@@ -71,12 +120,17 @@ def build_endpoint_class(
71
120
  }
72
121
 
73
122
  class EndpointClass(EndpointClassProtocol):
123
+ path_params_model = path_params_constructor # type: ignore
124
+ payload_model = payload_constructor # type: ignore
125
+ response_model = response_model_type
126
+
74
127
  @staticmethod
75
128
  def create_url(
76
- *args: PathParamsSpec.args, **kwargs: PathParamsSpec.kwargs
129
+ *args: PathParamsSpec.args,
130
+ **kwargs: PathParamsSpec.kwargs,
77
131
  ) -> str:
78
132
  """Create URL from path parameters."""
79
- path_model = path_params_model(*args, **kwargs)
133
+ path_model = EndpointClass.path_params_model(*args, **kwargs)
80
134
  path_dict = path_model.model_dump(**dump_options)
81
135
 
82
136
  # Extract expected path parameters from template
@@ -95,17 +149,29 @@ def build_endpoint_class(
95
149
 
96
150
  return url_template.substitute(**path_dict)
97
151
 
152
+ @staticmethod
153
+ def create_url_from_model(path_params: PathParamsType) -> str:
154
+ return url_template.substitute(**path_params.model_dump(**dump_options))
155
+
98
156
  @staticmethod
99
157
  def create_payload(
100
- *args: RequestBodySpec.args, **kwargs: RequestBodySpec.kwargs
158
+ *args: PayloadParamSpec.args, **kwargs: PayloadParamSpec.kwargs
101
159
  ) -> dict[str, Any]:
102
160
  """Create request body payload."""
103
- request_model = payload_model(*args, **kwargs)
161
+ request_model = EndpointClass.payload_model(*args, **kwargs)
104
162
  return request_model.model_dump(**dump_options)
105
163
 
164
+ @staticmethod
165
+ def create_payload_from_model(payload: PayloadType) -> dict[str, Any]:
166
+ return payload.model_dump(**dump_options)
167
+
106
168
  @staticmethod
107
169
  def handle_response(response: dict[str, Any]) -> ResponseType:
108
- return response_model.model_validate(response)
170
+ return EndpointClass.response_model.model_validate(response)
171
+
172
+ @staticmethod
173
+ def request_method() -> EndpointMethods:
174
+ return method
109
175
 
110
176
  return EndpointClass
111
177
 
@@ -131,9 +197,10 @@ if __name__ == "__main__":
131
197
  # Example usage of make_endpoint_class
132
198
  UserEndpoint = build_endpoint_class(
133
199
  url_template=Template("/users/${user_id}"),
134
- path_params_model=GetUserPathParams,
135
- payload_model=GetUserRequestBody,
136
- response_model=UserResponse,
200
+ path_params_constructor=GetUserPathParams,
201
+ payload_constructor=GetUserRequestBody,
202
+ response_model_type=UserResponse,
203
+ method=EndpointMethods.GET,
137
204
  )
138
205
 
139
206
  # Create URL from path parameters
@@ -143,3 +210,12 @@ if __name__ == "__main__":
143
210
  # Create payload from request body parameters
144
211
  payload = UserEndpoint.create_payload(include_profile=True)
145
212
  print(f"Payload: {payload}")
213
+
214
+ # Create response from endpoint
215
+ response = UserEndpoint.handle_response(
216
+ {
217
+ "id": 123,
218
+ "name": "John Doe",
219
+ }
220
+ )
221
+ print(f"Response: {response}")
@@ -0,0 +1,186 @@
1
+ from enum import StrEnum
2
+ from typing import Any, Callable, Generic, Protocol, TypeVar
3
+
4
+ from pydantic import BaseModel
5
+ from typing_extensions import ParamSpec
6
+
7
+ from unique_toolkit._common.endpoint_builder import (
8
+ EndpointClassProtocol,
9
+ EndpointMethods,
10
+ PathParamsSpec,
11
+ PathParamsType,
12
+ PayloadParamSpec,
13
+ PayloadType,
14
+ ResponseType,
15
+ )
16
+
17
+ # Paramspecs
18
+ CombinedParamsSpec = ParamSpec("CombinedParamsSpec")
19
+
20
+ # Type variables
21
+ CombinedParamsType = TypeVar("CombinedParamsType", bound=BaseModel)
22
+
23
+
24
+ class EndpointRequestorProtocol(Protocol, Generic[CombinedParamsSpec, ResponseType]):
25
+ @classmethod
26
+ def request(
27
+ cls,
28
+ headers: dict[str, str],
29
+ *args: CombinedParamsSpec.args,
30
+ **kwargs: CombinedParamsSpec.kwargs,
31
+ ) -> ResponseType: ...
32
+
33
+
34
+ def build_fake_requestor(
35
+ endpoint_type: type[
36
+ EndpointClassProtocol[
37
+ PathParamsSpec,
38
+ PathParamsType,
39
+ PayloadParamSpec,
40
+ PayloadType,
41
+ ResponseType,
42
+ ]
43
+ ],
44
+ combined_model: Callable[CombinedParamsSpec, CombinedParamsType],
45
+ return_value: dict[str, Any],
46
+ ) -> type[EndpointRequestorProtocol[CombinedParamsSpec, ResponseType]]:
47
+ class FakeRequestor(EndpointRequestorProtocol):
48
+ _endpoint = endpoint_type
49
+
50
+ @classmethod
51
+ def request(
52
+ cls,
53
+ headers: dict[str, str],
54
+ *args: CombinedParamsSpec.args,
55
+ **kwargs: CombinedParamsSpec.kwargs,
56
+ ) -> ResponseType:
57
+ try:
58
+ combined_model(*args, **kwargs)
59
+ except Exception as e:
60
+ raise ValueError(
61
+ f"Invalid parameters passed to combined model {combined_model.__name__}: {e}"
62
+ )
63
+
64
+ return cls._endpoint.handle_response(return_value)
65
+
66
+ return FakeRequestor
67
+
68
+
69
+ def build_request_requestor(
70
+ endpoint_type: type[
71
+ EndpointClassProtocol[
72
+ PathParamsSpec,
73
+ PathParamsType,
74
+ PayloadParamSpec,
75
+ PayloadType,
76
+ ResponseType,
77
+ ]
78
+ ],
79
+ combined_model: Callable[CombinedParamsSpec, CombinedParamsType],
80
+ ) -> type[EndpointRequestorProtocol]:
81
+ import requests
82
+
83
+ class RequestRequestor(EndpointRequestorProtocol):
84
+ _endpoint = endpoint_type
85
+
86
+ @classmethod
87
+ def request(
88
+ cls,
89
+ headers: dict[str, str],
90
+ *args: CombinedParamsSpec.args,
91
+ **kwargs: CombinedParamsSpec.kwargs,
92
+ ) -> ResponseType:
93
+ url = cls._endpoint.create_url_from_model(combined_model(*args, **kwargs))
94
+ payload = cls._endpoint.create_payload_from_model(
95
+ combined_model(*args, **kwargs)
96
+ )
97
+
98
+ response = requests.request(
99
+ method=cls._endpoint.request_method(),
100
+ url=url,
101
+ headers=headers,
102
+ json=payload,
103
+ )
104
+ return cls._endpoint.handle_response(response.json())
105
+
106
+ return RequestRequestor
107
+
108
+
109
+ class RequestorType(StrEnum):
110
+ REQUESTS = "requests"
111
+ FAKE = "fake"
112
+
113
+
114
+ def build_requestor(
115
+ requestor_type: RequestorType,
116
+ endpoint_type: type[
117
+ EndpointClassProtocol[
118
+ PathParamsSpec,
119
+ PathParamsType,
120
+ PayloadParamSpec,
121
+ PayloadType,
122
+ ResponseType,
123
+ ]
124
+ ],
125
+ combined_model: Callable[CombinedParamsSpec, CombinedParamsType],
126
+ return_value: dict[str, Any] | None = None,
127
+ **kwargs: Any,
128
+ ) -> type[EndpointRequestorProtocol]:
129
+ match requestor_type:
130
+ case RequestorType.REQUESTS:
131
+ return build_request_requestor(
132
+ endpoint_type=endpoint_type, combined_model=combined_model
133
+ )
134
+ case RequestorType.FAKE:
135
+ if return_value is None:
136
+ raise ValueError("return_value is required for fake requestor")
137
+ return build_fake_requestor(
138
+ endpoint_type=endpoint_type,
139
+ combined_model=combined_model,
140
+ return_value=return_value,
141
+ )
142
+
143
+
144
+ if __name__ == "__main__":
145
+ from string import Template
146
+
147
+ from unique_toolkit._common.endpoint_builder import build_endpoint_class
148
+
149
+ class GetUserPathParams(BaseModel):
150
+ user_id: int
151
+
152
+ class GetUserRequestBody(BaseModel):
153
+ include_profile: bool = False
154
+
155
+ class UserResponse(BaseModel):
156
+ id: int
157
+ name: str
158
+
159
+ class CombinedParams(GetUserPathParams, GetUserRequestBody):
160
+ pass
161
+
162
+ UserEndpoint = build_endpoint_class(
163
+ method=EndpointMethods.GET,
164
+ url_template=Template("https://api.example.com/users/{user_id}"),
165
+ path_params_constructor=GetUserPathParams,
166
+ payload_constructor=GetUserRequestBody,
167
+ response_model_type=UserResponse,
168
+ )
169
+
170
+ FakeUserRequestor = build_fake_requestor(
171
+ endpoint_type=UserEndpoint,
172
+ combined_model=CombinedParams,
173
+ return_value={"id": 100, "name": "John Doe"},
174
+ )
175
+
176
+ # Note that the return value is a pydantic UserResponse object
177
+ response = FakeUserRequestor().request(
178
+ headers={"a": "b"},
179
+ user_id=123,
180
+ include_profile=True,
181
+ )
182
+
183
+ print(response.model_dump())
184
+ print(response.model_json_schema())
185
+ print(response.id)
186
+ print(response.name)
@@ -68,7 +68,11 @@ class UniqueApi(BaseSettings):
68
68
  default="http://localhost:8092/",
69
69
  description="The base URL of the Unique API. Ask your admin to provide you with the correct URL.",
70
70
  validation_alias=AliasChoices(
71
- "unique_api_base_url", "base_url", "UNIQUE_API_BASE_URL", "BASE_URL"
71
+ "unique_api_base_url",
72
+ "base_url",
73
+ "UNIQUE_API_BASE_URL",
74
+ "BASE_URL",
75
+ "API_BASE",
72
76
  ),
73
77
  )
74
78
  version: str = Field(
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_toolkit
3
- Version: 0.8.42
3
+ Version: 0.8.44
4
4
  Summary:
5
5
  License: Proprietary
6
- Author: Martin Fadler
7
- Author-email: martin.fadler@unique.ch
6
+ Author: Cedric Klinkert
7
+ Author-email: cedric.klinkert@unique.ch
8
8
  Requires-Python: >=3.11,<4.0
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
@@ -117,6 +117,12 @@ All notable changes to this project will be documented in this file.
117
117
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
118
118
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
119
119
 
120
+ ## [0.8.44] - 2025-09-03
121
+ - Refine `EndpointClass` and create `EndpointRequestor`
122
+
123
+ ## [0.8.43] - 2025-09-03
124
+ - Add alias for `UniqueSettings` api base `API_BASE`
125
+
120
126
  ## [0.8.42] - 2025-09-02
121
127
  - updated schema of `chunk_relevancy_sorter`
122
128
 
@@ -8,7 +8,8 @@ unique_toolkit/_common/chunk_relevancy_sorter/schemas.py,sha256=doAWPPx8d0zIqHMX
8
8
  unique_toolkit/_common/chunk_relevancy_sorter/service.py,sha256=UxYn4xJMNEXQ1afMiT2sMwXgdmlFNPaglVhx6CRRtiM,13864
9
9
  unique_toolkit/_common/chunk_relevancy_sorter/tests/test_service.py,sha256=JRvLtJXPmz6bm1tFSSqt01HHVeanSD8zk70XVLJHOEM,8878
10
10
  unique_toolkit/_common/default_language_model.py,sha256=tmHSqg6e8G7RmKqmdE_tmLxkSN0x-aGoyUdy6Pl2oAE,334
11
- unique_toolkit/_common/endpoint_builder.py,sha256=oM6uDmxUqTAJut6MuJQj3bIX4yOccyErWD5bJ1d1lcY,4526
11
+ unique_toolkit/_common/endpoint_builder.py,sha256=zZFEeMAHySWh2AU-nuufT7iNd0NT54qA6AAxNEVDZb0,6635
12
+ unique_toolkit/_common/endpoint_requestor.py,sha256=q-H4kT2Ip63omsodMZ8Bgo3HLMFhnOpaGoc2MqIZdmI,5317
12
13
  unique_toolkit/_common/exception.py,sha256=caQIE1btsQnpKCHqL2cgWUSbHup06enQu_Pt7uGUTTE,727
13
14
  unique_toolkit/_common/feature_flags/schema.py,sha256=3JpTuld8kK-UQ5B0sbYTu0yqhyFPnChXG2Iv4BNqHdg,539
14
15
  unique_toolkit/_common/pydantic_helpers.py,sha256=4a8LPey31k4dCztYag1OBhYnGHREN08-l3NEjbFD1ok,743
@@ -24,7 +25,7 @@ unique_toolkit/app/init_sdk.py,sha256=5_oDoETr6akwYyBCb0ivTdMNu3SVgPSkrXcDS6ELyY
24
25
  unique_toolkit/app/performance/async_tasks.py,sha256=H0l3OAcosLwNHZ8d2pd-Di4wHIXfclEvagi5kfqLFPA,1941
25
26
  unique_toolkit/app/performance/async_wrapper.py,sha256=yVVcRDkcdyfjsxro-N29SBvi-7773wnfDplef6-y8xw,1077
26
27
  unique_toolkit/app/schemas.py,sha256=xHdzMyZ_cgCzxCqzCJYwOCAYWkaQ9zIyZsRhDgSQnEA,8088
27
- unique_toolkit/app/unique_settings.py,sha256=_Ll_GuWYiOIK2w4HQPA_uopTdDDFcjVqDFO3m2F2b-Q,10254
28
+ unique_toolkit/app/unique_settings.py,sha256=3BSMRAUz2rfbmIW5Ot-xXj7hMjWwtNskbUh9joh11mE,10315
28
29
  unique_toolkit/app/verification.py,sha256=GxFFwcJMy25fCA_Xe89wKW7bgqOu8PAs5y8QpHF0GSc,3861
29
30
  unique_toolkit/chat/__init__.py,sha256=LRs2G-JTVuci4lbtHTkVUiNcZcSR6uqqfnAyo7af6nY,619
30
31
  unique_toolkit/chat/constants.py,sha256=05kq6zjqUVB2d6_P7s-90nbljpB3ryxwCI-CAz0r2O4,83
@@ -131,7 +132,7 @@ unique_toolkit/tools/utils/execution/execution.py,sha256=vjG2Y6awsGNtlvyQAGCTthQ
131
132
  unique_toolkit/tools/utils/source_handling/schema.py,sha256=vzAyf6ZWNexjMO0OrnB8y2glGkvAilmGGQXd6zcDaKw,870
132
133
  unique_toolkit/tools/utils/source_handling/source_formatting.py,sha256=C7uayNbdkNVJdEARA5CENnHtNY1SU6etlaqbgHNyxaQ,9152
133
134
  unique_toolkit/tools/utils/source_handling/tests/test_source_formatting.py,sha256=oM5ZxEgzROrnX1229KViCAFjRxl9wCTzWZoinYSHleM,6979
134
- unique_toolkit-0.8.42.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
135
- unique_toolkit-0.8.42.dist-info/METADATA,sha256=QQPAhHh0vIc_d9P6DrmlSBSkhVLnCmqs8mFjVlPyHc4,30418
136
- unique_toolkit-0.8.42.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
137
- unique_toolkit-0.8.42.dist-info/RECORD,,
135
+ unique_toolkit-0.8.44.dist-info/LICENSE,sha256=GlN8wHNdh53xwOPg44URnwag6TEolCjoq3YD_KrWgss,193
136
+ unique_toolkit-0.8.44.dist-info/METADATA,sha256=qgsVibZKJU2Colr_5lqRIr2glNjV_48wFO_xqjnskF0,30583
137
+ unique_toolkit-0.8.44.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
138
+ unique_toolkit-0.8.44.dist-info/RECORD,,