cognick 0.0.1__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.
cognick/__init__.py ADDED
File without changes
cognick/caching.py ADDED
@@ -0,0 +1,27 @@
1
+ import json
2
+ import redis
3
+ from django.conf import settings
4
+
5
+ redis_host = settings.REDIS_HOST
6
+ redis_port = settings.REDIS_PORT
7
+ redis_select_db = settings.REDIS_SELECT_DB
8
+
9
+ class RedisCache:
10
+ def __init__(self):
11
+ self.client = redis.Redis(
12
+ host=redis_host,
13
+ port=redis_port,
14
+ db=redis_select_db,
15
+ decode_responses=True
16
+ )
17
+
18
+ def get_data(self, cache_key):
19
+ data = self.client.get(cache_key)
20
+ return json.loads(data) if data else None
21
+
22
+ def set_data(self, cache_key, data, timeout=3600):
23
+ self.client.set(
24
+ cache_key,
25
+ json.dumps(data),
26
+ ex=timeout
27
+ )
File without changes
@@ -0,0 +1,84 @@
1
+ from typing import Any
2
+
3
+ import requests
4
+ from django.conf import settings
5
+
6
+ from .exceptions import (
7
+ CognitiveConnectionException,
8
+ CognitiveNotFoundException,
9
+ CognitiveResponseException
10
+ )
11
+ from .objects import CognitiveParameter
12
+
13
+
14
+ class CognitiveClient:
15
+ """
16
+ Client for communicating with the cognitive service.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ timeout: int | None = None,
22
+ headers: dict[str, str] | None = None,
23
+ ):
24
+ self.url = self._get_url()
25
+ self.timeout = timeout or getattr(
26
+ settings,
27
+ "COGNICK_COGNITIVE_TIMEOUT",
28
+ 10,
29
+ )
30
+ self.headers = headers or {}
31
+
32
+ @staticmethod
33
+ def _get_url() -> str:
34
+ url = getattr(
35
+ settings,
36
+ "COGNICK_COGNITIVE_URL",
37
+ None,
38
+ )
39
+
40
+ if not url:
41
+ raise ValueError(
42
+ "COGNICK_COGNITIVE_BASE_URL must be configured "
43
+ "in Django settings."
44
+ )
45
+
46
+ return url.rstrip("/")
47
+
48
+ def get_parameter(
49
+ self
50
+ ) -> CognitiveParameter:
51
+ url = (
52
+ f"{self.url}/"
53
+ )
54
+
55
+ try:
56
+ response = requests.get(
57
+ url,
58
+ headers=self.headers,
59
+ timeout=self.timeout,
60
+ )
61
+ except requests.RequestException as exc:
62
+ raise CognitiveConnectionException(
63
+ f"Could not connect to cognitive service: {exc}"
64
+ ) from exc
65
+
66
+ if response.status_code == 404:
67
+ raise CognitiveNotFoundException(
68
+ f"Cognitive parameter was not found."
69
+ )
70
+
71
+ if not response.ok:
72
+ raise CognitiveResponseException(
73
+ f"Cognitive service returned status "
74
+ f"{response.status_code}."
75
+ )
76
+
77
+ try:
78
+ data: dict[str, Any] = response.json()
79
+ except ValueError as exc:
80
+ raise CognitiveResponseException(
81
+ "Cognitive service returned invalid JSON."
82
+ ) from exc
83
+
84
+ return CognitiveParameter(data)
@@ -0,0 +1,15 @@
1
+
2
+ class CognitiveException(Exception):
3
+ """Base exception for cognitive package."""
4
+
5
+
6
+ class CognitiveNotFoundException(CognitiveException):
7
+ """Raised when a cognitive parameter cannot be found."""
8
+
9
+
10
+ class CognitiveConnectionException(CognitiveException):
11
+ """Raised when the cognitive service cannot be reached."""
12
+
13
+
14
+ class CognitiveResponseException(CognitiveException):
15
+ """Raised when the cognitive service returns an invalid response."""
@@ -0,0 +1,317 @@
1
+ from typing import Any
2
+
3
+ from django.contrib.postgres.fields import ArrayField
4
+ from django.db import models
5
+
6
+ from .utils import get_all_data
7
+ from .objects import CognitiveParameter
8
+
9
+
10
+ class CognitiveFieldDescriptor:
11
+ """
12
+ Lazily resolves a cognitive parameter ID to a CognitiveParameter object.
13
+ """
14
+
15
+ def __init__(self, field):
16
+ self.field = field
17
+ self.attname = field.attname
18
+
19
+ def __get__(self, instance, owner=None):
20
+ if instance is None:
21
+ return self
22
+
23
+ value = instance.__dict__.get(self.attname)
24
+
25
+ if value is None:
26
+ return None
27
+
28
+ if isinstance(value, CognitiveParameter):
29
+ return value
30
+
31
+ cognitive = self.field.get_full_data(value)
32
+
33
+ instance.__dict__[self.attname] = cognitive
34
+
35
+ return cognitive
36
+
37
+ def __set__(self, instance, value):
38
+ instance.__dict__[self.attname] = value
39
+
40
+
41
+ class CognitiveArrayFieldDescriptor:
42
+ """
43
+ Lazily resolves cognitive parameter IDs to CognitiveParameter objects.
44
+ """
45
+
46
+ def __init__(self, field):
47
+ self.field = field
48
+ self.attname = field.attname
49
+
50
+ def __get__(self, instance, owner=None):
51
+ if instance is None:
52
+ return self
53
+
54
+ value = instance.__dict__.get(self.attname)
55
+
56
+ if value is None:
57
+ return None
58
+
59
+ if not isinstance(value, (list, tuple)):
60
+ return value
61
+
62
+ if all(
63
+ isinstance(item, CognitiveParameter)
64
+ for item in value
65
+ ):
66
+ return value
67
+
68
+ resolved = self.field.get_full_data_many(value)
69
+
70
+ instance.__dict__[self.attname] = resolved
71
+
72
+ return resolved
73
+
74
+ def __set__(self, instance, value):
75
+ instance.__dict__[self.attname] = value
76
+
77
+
78
+ class CognitiveField(models.CharField):
79
+ """
80
+ Stores a cognitive parameter ID in the database.
81
+
82
+ The field accepts either a cognitive parameter ID or a
83
+ CognitiveParameter instance when saving.
84
+ """
85
+
86
+ descriptor_class = CognitiveFieldDescriptor
87
+
88
+ def __init__(
89
+ self,
90
+ *args,
91
+ cache_timeout: int | None = None,
92
+ timeout: int | None = None,
93
+ **kwargs,
94
+ ):
95
+ kwargs.setdefault("max_length", 36)
96
+
97
+ self.cache_timeout = cache_timeout
98
+ self.timeout = timeout
99
+
100
+ super().__init__(*args, **kwargs)
101
+
102
+ def get_prep_value(
103
+ self,
104
+ value: CognitiveParameter | str | int | None,
105
+ ) -> str | None:
106
+ if value is None:
107
+ return None
108
+
109
+ if isinstance(value, CognitiveParameter):
110
+ return str(value.id)
111
+
112
+ if isinstance(value, (str, int)):
113
+ return str(value)
114
+
115
+ raise ValueError(
116
+ "CognitiveField only accepts a cognitive parameter ID "
117
+ "or a CognitiveParameter instance."
118
+ )
119
+
120
+ def from_db_value(
121
+ self,
122
+ value: Any,
123
+ expression,
124
+ connection,
125
+ ) -> str | None:
126
+ if value is None:
127
+ return None
128
+
129
+ return str(value)
130
+
131
+
132
+ def get_full_data(
133
+ self,
134
+ value: str | int | CognitiveParameter | None,
135
+ ) -> CognitiveParameter | None:
136
+ """
137
+ Resolve a cognitive parameter ID to a CognitiveParameter object.
138
+ """
139
+
140
+ if value is None:
141
+ return None
142
+
143
+ if isinstance(value, CognitiveParameter):
144
+ return value
145
+
146
+ value = str(value)
147
+
148
+ cognitive_data = get_all_data(
149
+ self.cache_timeout,
150
+ self.timeout
151
+ )
152
+
153
+ data = cognitive_data.get(value)
154
+
155
+ if data is None:
156
+ raise ValueError(
157
+ f"Cognitive parameter with ID '{value}' was not found."
158
+ )
159
+
160
+ return CognitiveParameter(data)
161
+
162
+ def get_full_data_many(
163
+ self,
164
+ values: list[str | int | CognitiveParameter] | tuple,
165
+ ) -> list[CognitiveParameter]:
166
+ """
167
+ Resolve multiple cognitive parameter IDs using the shared cache.
168
+ """
169
+
170
+ if not values:
171
+ return []
172
+
173
+ cognitive_data = get_all_data(
174
+ self.cache_timeout,
175
+ self.timeout
176
+ )
177
+
178
+ resolved = []
179
+
180
+ for value in values:
181
+ if isinstance(value, CognitiveParameter):
182
+ resolved.append(value)
183
+ continue
184
+
185
+ value = str(value)
186
+
187
+ data = cognitive_data.get(value)
188
+
189
+ if data is None:
190
+ raise ValueError(
191
+ f"Cognitive parameter with ID '{value}' was not found."
192
+ )
193
+
194
+ resolved.append(
195
+ CognitiveParameter(data)
196
+ )
197
+
198
+ return resolved
199
+
200
+
201
+ class CognitiveArrayField(ArrayField):
202
+ """
203
+ Stores a list of cognitive parameter IDs in the database.
204
+
205
+ Values are stored as IDs and lazily resolved to CognitiveParameter
206
+ objects when the model attribute is accessed.
207
+ """
208
+
209
+ descriptor_class = CognitiveArrayFieldDescriptor
210
+
211
+ def __init__(
212
+ self,
213
+ base_field=None,
214
+ *args,
215
+ cache_timeout: int | None = None,
216
+ timeout: int | None = None,
217
+ **kwargs,
218
+ ):
219
+ self.cache_timeout = cache_timeout
220
+ self.timeout = timeout
221
+
222
+ if base_field is None:
223
+ base_field = CognitiveField(
224
+ cache_timeout=cache_timeout,
225
+ timeout=timeout,
226
+ )
227
+
228
+ super().__init__(
229
+ base_field=base_field,
230
+ *args,
231
+ **kwargs,
232
+ )
233
+
234
+ def get_full_data(
235
+ self,
236
+ value: str | int | CognitiveParameter | None,
237
+ ) -> CognitiveParameter | None:
238
+ """
239
+ Resolve a cognitive parameter ID to a CognitiveParameter object.
240
+ """
241
+
242
+ if value is None:
243
+ return None
244
+
245
+ if isinstance(value, CognitiveParameter):
246
+ return value
247
+
248
+ if isinstance(self.base_field, CognitiveField):
249
+ return self.base_field.get_full_data(value)
250
+
251
+ cognitive_field = CognitiveField(
252
+ cache_timeout=self.cache_timeout,
253
+ timeout=self.timeout,
254
+ )
255
+
256
+ return cognitive_field.get_full_data(value)
257
+
258
+ def get_full_data_many(
259
+ self,
260
+ values: list[str | int | CognitiveParameter] | tuple,
261
+ ) -> list[CognitiveParameter]:
262
+ """
263
+ Resolve multiple cognitive parameter IDs using the shared cache.
264
+ """
265
+
266
+ if not values:
267
+ return []
268
+
269
+ if isinstance(self.base_field, CognitiveField):
270
+ return self.base_field.get_full_data_many(values)
271
+
272
+ cognitive_field = CognitiveField(
273
+ cache_timeout=self.cache_timeout,
274
+ timeout=self.timeout,
275
+ )
276
+
277
+ return cognitive_field.get_full_data_many(values)
278
+
279
+ def get_prep_value(
280
+ self,
281
+ value: Any,
282
+ ) -> list[str] | None:
283
+ """
284
+ Prepare cognitive parameter values for database storage.
285
+ """
286
+
287
+ if value is None:
288
+ return None
289
+
290
+ if not isinstance(value, (list, tuple)):
291
+ raise ValueError(
292
+ "CognitiveArrayField only accepts a list or tuple."
293
+ )
294
+
295
+ return [
296
+ self.get_prep_value_item(item)
297
+ for item in value
298
+ ]
299
+
300
+ def get_prep_value_item(
301
+ self,
302
+ value: str | int | CognitiveParameter,
303
+ ) -> str:
304
+ """
305
+ Prepare a single cognitive parameter value for database storage.
306
+ """
307
+
308
+ if isinstance(value, CognitiveParameter):
309
+ return str(value.id)
310
+
311
+ if isinstance(value, (str, int)):
312
+ return str(value)
313
+
314
+ raise ValueError(
315
+ "CognitiveArrayField items must be cognitive parameter IDs "
316
+ "or CognitiveParameter instances."
317
+ )
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+
4
+ class CognitiveParameter:
5
+ """
6
+ Represents a cognitive parameter retrieved from the cognitive service.
7
+ """
8
+
9
+ def __init__(self, payload: dict[str, Any]):
10
+ self._payload = payload
11
+
12
+ def __getattr__(self, name: str) -> Any:
13
+ try:
14
+ return self._payload[name]
15
+ except KeyError as exc:
16
+ raise AttributeError(
17
+ f"{self.__class__.__name__!s} object has no attribute {name!r}"
18
+ ) from exc
19
+
20
+ def __getitem__(self, key: str) -> Any:
21
+ return self._payload[key]
22
+
23
+ def get(self, key: str, default: Any = None) -> Any:
24
+ return self._payload.get(key, default)
25
+
26
+ @property
27
+ def payload(self) -> dict[str, Any]:
28
+ return self._payload
29
+
30
+ @property
31
+ def id(self) -> str | int | None:
32
+ return self._payload.get("id")
33
+
34
+ def __bool__(self) -> bool:
35
+ return bool(self._payload)
36
+
37
+ def __repr__(self) -> str:
38
+ return f"<CognitiveParameter(id={self.id})>"
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ from rest_framework import serializers
4
+ from .objects import CognitiveParameter
5
+
6
+
7
+ class CognitiveParameterSerializer(serializers.Serializer):
8
+ """
9
+ Dynamically serializes a CognitiveParameter payload.
10
+ """
11
+
12
+ def to_representation(self, instance: CognitiveParameter) -> dict[str, Any]:
13
+ if isinstance(instance, CognitiveParameter):
14
+ return instance.payload.copy()
15
+
16
+ if isinstance(instance, dict):
17
+ return instance.copy()
18
+
19
+ return super().to_representation(instance)
@@ -0,0 +1,67 @@
1
+ from typing import Any
2
+ from django.conf import settings
3
+
4
+ from caching import RedisCache
5
+ from .client import CognitiveClient
6
+
7
+
8
+
9
+ def get_all_data(
10
+ cache_timeout,
11
+ timeout
12
+ ) -> dict[str, dict[str, Any]]:
13
+ """
14
+ Get all cognitive parameters indexed by their ID.
15
+
16
+ The complete list is cached so individual cognitive parameter
17
+ lookups do not require separate API requests.
18
+ """
19
+ all_cache_key = "cognick:cognitive:all"
20
+
21
+ cache = RedisCache()
22
+ cached_data = cache.get_data(all_cache_key)
23
+
24
+ if cached_data is not None:
25
+ return cached_data
26
+
27
+ client = CognitiveClient(
28
+ timeout=timeout,
29
+ )
30
+
31
+ response = client.get_parameter()
32
+
33
+ data = response.payload
34
+
35
+ if isinstance(data, dict):
36
+ parameters = data.get("results", data.get("data", []))
37
+ else:
38
+ parameters = data
39
+
40
+ if not isinstance(parameters, list):
41
+ raise ValueError(
42
+ "Cognitive API returned an invalid response format."
43
+ )
44
+
45
+ cognitive_data = {
46
+ str(item["id"]): item
47
+ for item in parameters
48
+ if isinstance(item, dict) and item.get("id") is not None
49
+ }
50
+
51
+ cache_timeout = (
52
+ cache_timeout
53
+ if cache_timeout is not None
54
+ else getattr(
55
+ settings,
56
+ "COGNICK_COGNITIVE_CACHE_TIMEOUT",
57
+ 3600,
58
+ )
59
+ )
60
+
61
+ cache.set_data(
62
+ all_cache_key,
63
+ cognitive_data,
64
+ timeout=cache_timeout,
65
+ )
66
+
67
+ return cognitive_data