qwak-core 0.4.378__py3-none-any.whl → 0.5.4__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.

Potentially problematic release.


This version of qwak-core might be problematic. Click here for more details.

Files changed (29) hide show
  1. _qwak_proto/qwak/administration/account/v1/account_pb2.py +20 -18
  2. _qwak_proto/qwak/administration/account/v1/account_pb2.pyi +21 -2
  3. _qwak_proto/qwak/admiral/secret/v0/secret_pb2.py +16 -14
  4. _qwak_proto/qwak/admiral/secret/v0/secret_pb2.pyi +21 -2
  5. _qwak_proto/qwak/builds/build_values_pb2.py +24 -18
  6. _qwak_proto/qwak/builds/build_values_pb2.pyi +21 -1
  7. _qwak_proto/qwak/execution/v1/streaming_aggregation_pb2.py +18 -11
  8. _qwak_proto/qwak/execution/v1/streaming_aggregation_pb2.pyi +71 -1
  9. _qwak_proto/qwak/feature_store/features/feature_set_pb2.py +4 -4
  10. _qwak_proto/qwak/feature_store/features/feature_set_pb2.pyi +4 -0
  11. _qwak_proto/qwak/feature_store/features/feature_set_types_pb2.py +60 -58
  12. _qwak_proto/qwak/feature_store/features/feature_set_types_pb2.pyi +7 -2
  13. _qwak_proto/qwak/kube_deployment_captain/batch_job_pb2.py +40 -40
  14. _qwak_proto/qwak/kube_deployment_captain/batch_job_pb2.pyi +7 -1
  15. _qwak_proto/qwak/projects/projects_pb2.py +17 -15
  16. _qwak_proto/qwak/secret_service/secret_service_pb2.pyi +1 -1
  17. qwak/__init__.py +1 -1
  18. qwak/exceptions/__init__.py +1 -0
  19. qwak/exceptions/qwak_grpc_address_exception.py +9 -0
  20. qwak/inner/const.py +2 -6
  21. qwak/inner/di_configuration/__init__.py +1 -67
  22. qwak/inner/di_configuration/dependency_wiring.py +98 -0
  23. qwak/inner/tool/grpc/grpc_tools.py +123 -3
  24. qwak/llmops/generation/chat/openai/types/chat/chat_completion.py +24 -6
  25. qwak/llmops/generation/chat/openai/types/chat/chat_completion_chunk.py +44 -8
  26. qwak/llmops/generation/chat/openai/types/chat/chat_completion_message.py +6 -3
  27. {qwak_core-0.4.378.dist-info → qwak_core-0.5.4.dist-info}/METADATA +4 -6
  28. {qwak_core-0.4.378.dist-info → qwak_core-0.5.4.dist-info}/RECORD +29 -27
  29. {qwak_core-0.4.378.dist-info → qwak_core-0.5.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,98 @@
1
+ import os
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ from qwak.inner.const import QwakConstants
6
+ from qwak.inner.di_configuration import QwakContainer
7
+ from qwak.inner.tool.grpc.grpc_tools import validate_grpc_address
8
+ from qwak.tools.logger import get_qwak_logger
9
+
10
+
11
+ logger = get_qwak_logger()
12
+
13
+ __DEFAULT_CONFIG_FILE_PATH: Path = Path(__file__).parent / "config.yml"
14
+
15
+
16
+ def wire_dependencies():
17
+ container = QwakContainer()
18
+
19
+ container.config.from_yaml(__DEFAULT_CONFIG_FILE_PATH)
20
+ control_plane_grpc_address_override: Optional[str] = os.getenv(
21
+ QwakConstants.CONTROL_PLANE_GRPC_ADDRESS_ENVAR_NAME
22
+ )
23
+
24
+ if control_plane_grpc_address_override:
25
+ validate_grpc_address(control_plane_grpc_address_override)
26
+ __override_control_plane_grpc_address(
27
+ container, control_plane_grpc_address_override
28
+ )
29
+
30
+ from qwak.clients import (
31
+ administration,
32
+ alert_management,
33
+ alerts_registry,
34
+ analytics,
35
+ audience,
36
+ automation_management,
37
+ autoscaling,
38
+ batch_job_management,
39
+ build_orchestrator,
40
+ data_versioning,
41
+ deployment,
42
+ feature_store,
43
+ file_versioning,
44
+ instance_template,
45
+ integration_management,
46
+ kube_deployment_captain,
47
+ logging_client,
48
+ model_management,
49
+ project,
50
+ prompt_manager,
51
+ system_secret,
52
+ user_application_instance,
53
+ vector_store,
54
+ workspace_manager,
55
+ )
56
+
57
+ container.wire(
58
+ packages=[
59
+ administration,
60
+ alert_management,
61
+ audience,
62
+ automation_management,
63
+ autoscaling,
64
+ analytics,
65
+ batch_job_management,
66
+ build_orchestrator,
67
+ data_versioning,
68
+ deployment,
69
+ file_versioning,
70
+ instance_template,
71
+ kube_deployment_captain,
72
+ logging_client,
73
+ model_management,
74
+ project,
75
+ feature_store,
76
+ user_application_instance,
77
+ alerts_registry,
78
+ workspace_manager,
79
+ vector_store,
80
+ integration_management,
81
+ system_secret,
82
+ prompt_manager,
83
+ ]
84
+ )
85
+
86
+ return container
87
+
88
+
89
+ def __override_control_plane_grpc_address(
90
+ container: "QwakContainer", control_plane_grpc_address_override: str
91
+ ):
92
+ logger.debug(
93
+ "Overriding control plane gRPC address from environment variable to %s.",
94
+ control_plane_grpc_address_override,
95
+ )
96
+ container.config.grpc.core.address.from_value(
97
+ control_plane_grpc_address_override.strip()
98
+ )
@@ -1,15 +1,18 @@
1
1
  import logging
2
+ import re
2
3
  import time
3
4
  from abc import ABC, abstractmethod
4
5
  from random import randint
5
6
  from typing import Callable, Optional, Tuple
7
+ from urllib.parse import urlparse, ParseResult
6
8
 
7
9
  import grpc
8
- from qwak.exceptions import QwakException
9
10
 
11
+ from qwak.exceptions import QwakException, QwakGrpcAddressException
10
12
  from .grpc_auth import Auth0Client
11
13
 
12
14
  logger = logging.getLogger()
15
+ HOSTNAME_REGEX: str = r"^(?!-)(?:[A-Za-z0-9-]{1,63}\.)*[A-Za-z0-9-]{1,63}(?<!-)$"
13
16
 
14
17
 
15
18
  def create_grpc_channel(
@@ -19,7 +22,7 @@ def create_grpc_channel(
19
22
  auth_metadata_plugin: grpc.AuthMetadataPlugin = None,
20
23
  timeout: int = 100,
21
24
  options=None,
22
- backoff_options={},
25
+ backoff_options=None,
23
26
  max_attempts=4,
24
27
  status_for_retry=(grpc.StatusCode.UNAVAILABLE,),
25
28
  attempt=0,
@@ -40,6 +43,9 @@ def create_grpc_channel(
40
43
  status_for_retry: grpc statuses to retry upon
41
44
  Returns: Returns a grpc.Channel
42
45
  """
46
+ if backoff_options is None:
47
+ backoff_options = {}
48
+
43
49
  if not url:
44
50
  raise QwakException("Unable to create gRPC channel. URL has not been defined.")
45
51
 
@@ -101,11 +107,14 @@ def create_grpc_channel_or_none(
101
107
  auth_metadata_plugin: grpc.AuthMetadataPlugin = None,
102
108
  timeout: int = 30,
103
109
  options=None,
104
- backoff_options={},
110
+ backoff_options=None,
105
111
  max_attempts=2,
106
112
  status_for_retry=(grpc.StatusCode.UNAVAILABLE,),
107
113
  attempt=0,
108
114
  ) -> Callable[[Optional[str], Optional[bool]], Optional[grpc.Channel]]:
115
+ if backoff_options is None:
116
+ backoff_options = {}
117
+
109
118
  def deferred_channel(
110
119
  url_overwrite: Optional[str] = None, ssl_overwrite: Optional[bool] = None
111
120
  ):
@@ -129,6 +138,117 @@ def create_grpc_channel_or_none(
129
138
  return deferred_channel
130
139
 
131
140
 
141
+ def validate_grpc_address(
142
+ grpc_address: str,
143
+ is_port_specification_allowed: bool = False,
144
+ is_url_scheme_allowed: bool = False,
145
+ ):
146
+ """
147
+ Validate gRPC address format
148
+ Args:
149
+ grpc_address (str): gRPC address to validate
150
+ is_port_specification_allowed (bool): Whether to allow port specification in the address
151
+ is_url_scheme_allowed (bool): Whether to allow URL scheme in the address
152
+ Raises:
153
+ QwakGrpcAddressException: If the gRPC address is invalid
154
+ """
155
+ parsed_grpc_address: ParseResult = parse_address(grpc_address)
156
+ hostname: str = get_hostname_from_address(parsed_grpc_address)
157
+ validate_paths_are_not_included_in_address(parsed_grpc_address)
158
+
159
+ if not is_url_scheme_allowed:
160
+ __validate_url_scheme_not_included_in_address(parsed_grpc_address)
161
+
162
+ if not is_port_specification_allowed:
163
+ __validate_port_not_included_in_address(parsed_grpc_address)
164
+
165
+ if not is_valid_hostname(hostname):
166
+ raise QwakGrpcAddressException(
167
+ "gRPC address must be a simple hostname or fully qualified domain name.",
168
+ parsed_grpc_address,
169
+ )
170
+
171
+
172
+ def validate_paths_are_not_included_in_address(
173
+ parsed_grpc_address: ParseResult,
174
+ ) -> None:
175
+ has_invalid_path: bool = (
176
+ parsed_grpc_address.path not in {"", "/"}
177
+ or parsed_grpc_address.query
178
+ or parsed_grpc_address.fragment
179
+ )
180
+
181
+ if has_invalid_path:
182
+ raise QwakGrpcAddressException(
183
+ "gRPC address must not contain paths, queries, or fragments.",
184
+ parsed_grpc_address,
185
+ )
186
+
187
+
188
+ def get_hostname_from_address(parsed_grpc_address: ParseResult) -> str:
189
+ hostname: Optional[str] = parsed_grpc_address.hostname
190
+ if not hostname:
191
+ raise QwakGrpcAddressException(
192
+ "gRPC address must contain a valid hostname.", parsed_grpc_address
193
+ )
194
+
195
+ return hostname
196
+
197
+
198
+ def __validate_url_scheme_not_included_in_address(
199
+ parsed_grpc_address: ParseResult,
200
+ ) -> None:
201
+ if parsed_grpc_address.scheme:
202
+ raise QwakGrpcAddressException(
203
+ "URL scheme is not allowed in the gRPC address.", parsed_grpc_address
204
+ )
205
+
206
+
207
+ def __validate_port_not_included_in_address(parsed_grpc_address: ParseResult):
208
+ try:
209
+ port: Optional[int] = parsed_grpc_address.port
210
+ except ValueError as exc:
211
+ raise QwakGrpcAddressException(
212
+ "Invalid port specification in the gRPC address.", parsed_grpc_address
213
+ ) from exc
214
+
215
+ if port:
216
+ raise QwakGrpcAddressException(
217
+ "Port specification is not allowed in the gRPC address.",
218
+ parsed_grpc_address,
219
+ )
220
+
221
+
222
+ def parse_address(grpc_address: str) -> ParseResult:
223
+ if not grpc_address or not grpc_address.strip():
224
+ raise QwakGrpcAddressException(
225
+ "gRPC address must not be empty or whitespace.", grpc_address
226
+ )
227
+
228
+ trimmed_address: str = grpc_address.strip()
229
+ parsed_address: ParseResult = urlparse(
230
+ trimmed_address if "://" in trimmed_address else f"//{trimmed_address}"
231
+ )
232
+
233
+ return parsed_address
234
+
235
+
236
+ def is_valid_hostname(hostname: str) -> bool:
237
+ """
238
+ Validate that the supplied hostname conforms to RFC-style label rules:
239
+ anchored pattern enforces full-string validation, negative lookahead/lookbehind block
240
+ leading or trailing hyphens per label, and each dot-separated label must be 1-63
241
+ alphanumeric/hyphen characters.
242
+
243
+ Args:
244
+ hostname (str): The hostname to validate.
245
+ Returns:
246
+ bool: True if the hostname is valid, False otherwise.
247
+ """
248
+ hostname_pattern: re.Pattern = re.compile(HOSTNAME_REGEX)
249
+ return bool(hostname_pattern.fullmatch(hostname))
250
+
251
+
132
252
  class SleepingPolicy(ABC):
133
253
  @abstractmethod
134
254
  def sleep(self, try_i: int):
@@ -16,8 +16,6 @@
16
16
  from dataclasses import dataclass
17
17
  from typing import List, Optional
18
18
 
19
- from typing_extensions import Literal
20
-
21
19
  from qwak.llmops.generation.base import ModelResponse
22
20
  from .chat_completion_message import ChatCompletionMessage
23
21
  from .chat_completion_token_logprob import ChatCompletionTokenLogprob
@@ -34,9 +32,7 @@ class ChoiceLogprobs:
34
32
 
35
33
  @dataclass
36
34
  class Choice:
37
- finish_reason: Literal[
38
- "stop", "length", "tool_calls", "content_filter", "function_call"
39
- ]
35
+ finish_reason: str
40
36
  """The reason the model stopped generating tokens.
41
37
 
42
38
  This will be `stop` if the model hit a natural stop point or a provided stop
@@ -55,6 +51,21 @@ class Choice:
55
51
  logprobs: Optional[ChoiceLogprobs] = None
56
52
  """Log probability information for the choice."""
57
53
 
54
+ def __post_init__(self):
55
+ """Validates that finish_reason is one of the allowed values."""
56
+ allowed_reasons = {
57
+ "stop",
58
+ "length",
59
+ "tool_calls",
60
+ "content_filter",
61
+ "function_call",
62
+ }
63
+ if self.finish_reason not in allowed_reasons:
64
+ raise ValueError(
65
+ f"Invalid finish_reason: '{self.finish_reason}'. "
66
+ f"Must be one of {allowed_reasons}"
67
+ )
68
+
58
69
 
59
70
  @dataclass
60
71
  class ChatCompletion(ModelResponse):
@@ -73,7 +84,7 @@ class ChatCompletion(ModelResponse):
73
84
  model: str
74
85
  """The model used for the chat completion."""
75
86
 
76
- object: Literal["chat.completion"]
87
+ object: str
77
88
  """The object type, which is always `chat.completion`."""
78
89
 
79
90
  system_fingerprint: Optional[str] = None
@@ -85,3 +96,10 @@ class ChatCompletion(ModelResponse):
85
96
 
86
97
  usage: Optional[CompletionUsage] = None
87
98
  """Usage statistics for the completion request."""
99
+
100
+ def __post_init__(self):
101
+ """Validates that the object type is correct."""
102
+ if self.object != "chat.completion":
103
+ raise ValueError(
104
+ f"Invalid object type: '{self.object}'. Must be 'chat.completion'"
105
+ )
@@ -16,8 +16,6 @@
16
16
  from dataclasses import dataclass
17
17
  from typing import List, Optional
18
18
 
19
- from typing_extensions import Literal
20
-
21
19
  from qwak.llmops.generation.base import ModelResponse
22
20
  from .chat_completion_token_logprob import ChatCompletionTokenLogprob
23
21
 
@@ -69,9 +67,16 @@ class ChoiceDeltaToolCall:
69
67
 
70
68
  function: Optional[ChoiceDeltaToolCallFunction] = None
71
69
 
72
- type: Optional[Literal["function"]] = None
70
+ type: Optional[str] = None
73
71
  """The type of the tool. Currently, only `function` is supported."""
74
72
 
73
+ def __post_init__(self):
74
+ """Validates that type is 'function' if present."""
75
+ if self.type is not None and self.type != "function":
76
+ raise ValueError(
77
+ f"Invalid type: '{self.type}'. Must be 'function' or None."
78
+ )
79
+
75
80
 
76
81
  @dataclass
77
82
  class ChoiceDelta:
@@ -85,11 +90,21 @@ class ChoiceDelta:
85
90
  model.
86
91
  """
87
92
 
88
- role: Optional[Literal["system", "user", "assistant", "tool"]] = None
93
+ role: Optional[str] = None
89
94
  """The role of the author of this message."""
90
95
 
91
96
  tool_calls: Optional[List[ChoiceDeltaToolCall]] = None
92
97
 
98
+ def __post_init__(self):
99
+ """Validates that role is one of the allowed values if present."""
100
+ if self.role is not None:
101
+ allowed_roles = {"system", "user", "assistant", "tool"}
102
+ if self.role not in allowed_roles:
103
+ raise ValueError(
104
+ f"Invalid role: '{self.role}'. "
105
+ f"Must be one of {allowed_roles} or None."
106
+ )
107
+
93
108
 
94
109
  @dataclass
95
110
  class ChoiceLogprobs:
@@ -105,9 +120,7 @@ class Choice:
105
120
  index: int
106
121
  """The index of the choice in the list of choices."""
107
122
 
108
- finish_reason: Optional[
109
- Literal["stop", "length", "tool_calls", "content_filter", "function_call"]
110
- ] = None
123
+ finish_reason: Optional[str] = None
111
124
  """The reason the model stopped generating tokens.
112
125
 
113
126
  This will be `stop` if the model hit a natural stop point or a provided stop
@@ -120,6 +133,22 @@ class Choice:
120
133
  logprobs: Optional[ChoiceLogprobs] = None
121
134
  """Log probability information for the choice."""
122
135
 
136
+ def __post_init__(self):
137
+ """Validates that finish_reason is one of the allowed values if present."""
138
+ if self.finish_reason is not None:
139
+ allowed_reasons = {
140
+ "stop",
141
+ "length",
142
+ "tool_calls",
143
+ "content_filter",
144
+ "function_call",
145
+ }
146
+ if self.finish_reason not in allowed_reasons:
147
+ raise ValueError(
148
+ f"Invalid finish_reason: '{self.finish_reason}'. "
149
+ f"Must be one of {allowed_reasons} or None."
150
+ )
151
+
123
152
 
124
153
  @dataclass
125
154
  class ChatCompletionChunk(ModelResponse):
@@ -141,7 +170,7 @@ class ChatCompletionChunk(ModelResponse):
141
170
  model: str
142
171
  """The model to generate the completion."""
143
172
 
144
- object: Literal["chat.completion.chunk"]
173
+ object: str
145
174
  """The object type, which is always `chat.completion.chunk`."""
146
175
 
147
176
  system_fingerprint: Optional[str] = None
@@ -150,3 +179,10 @@ class ChatCompletionChunk(ModelResponse):
150
179
  Can be used in conjunction with the `seed` request parameter to understand when
151
180
  backend changes have been made that might impact determinism.
152
181
  """
182
+
183
+ def __post_init__(self):
184
+ """Validates that the object type is correct."""
185
+ if self.object != "chat.completion.chunk":
186
+ raise ValueError(
187
+ f"Invalid object type: '{self.object}'. Must be 'chat.completion.chunk'"
188
+ )
@@ -16,8 +16,6 @@
16
16
  from dataclasses import dataclass
17
17
  from typing import List, Optional
18
18
 
19
- from typing_extensions import Literal
20
-
21
19
  from .chat_completion_message_tool_call import ChatCompletionMessageToolCall
22
20
 
23
21
  __all__ = ["ChatCompletionMessage", "FunctionCall"]
@@ -39,7 +37,7 @@ class FunctionCall:
39
37
 
40
38
  @dataclass
41
39
  class ChatCompletionMessage:
42
- role: Literal["assistant"]
40
+ role: str
43
41
  """The role of the author of this message."""
44
42
 
45
43
  content: Optional[str] = None
@@ -54,3 +52,8 @@ class ChatCompletionMessage:
54
52
 
55
53
  tool_calls: Optional[List[ChatCompletionMessageToolCall]] = None
56
54
  """The tool calls generated by the model, such as function calls."""
55
+
56
+ def __post_init__(self):
57
+ """Validates that the role type is correct."""
58
+ if self.role != "assistant":
59
+ raise ValueError(f"Invalid object type: '{self.role}'. Must be 'assistant'")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qwak-core
3
- Version: 0.4.378
3
+ Version: 0.5.4
4
4
  Summary: Qwak Core contains the necessary objects and communication tools for using the Qwak Platform
5
5
  License: Apache-2.0
6
6
  Keywords: mlops,ml,deployment,serving,model
@@ -13,8 +13,6 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.7
17
- Classifier: Programming Language :: Python :: 3.8
18
16
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
17
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
18
  Provides-Extra: feature-store
@@ -22,7 +20,7 @@ Requires-Dist: PyYAML (>=6.0.2)
22
20
  Requires-Dist: cachetools
23
21
  Requires-Dist: chevron (==0.14.0)
24
22
  Requires-Dist: cloudpickle (==2.2.1) ; extra == "feature-store"
25
- Requires-Dist: dacite (==1.8.1)
23
+ Requires-Dist: dacite (==1.9.2)
26
24
  Requires-Dist: dependency-injector (>=4.0)
27
25
  Requires-Dist: filelock
28
26
  Requires-Dist: grpcio (>=1.71.2)
@@ -32,11 +30,11 @@ Requires-Dist: protobuf (>=4.25.8,<5)
32
30
  Requires-Dist: pyarrow (>=20.0.0) ; extra == "feature-store"
33
31
  Requires-Dist: pyathena (>=2.2.0,!=2.18.0) ; extra == "feature-store"
34
32
  Requires-Dist: pydantic
35
- Requires-Dist: pyspark (==3.4.2) ; extra == "feature-store"
33
+ Requires-Dist: pyspark (==3.5.7) ; extra == "feature-store"
36
34
  Requires-Dist: python-jose[cryptography] (>=3.4.0)
37
35
  Requires-Dist: python-json-logger (>=2.0.2)
38
36
  Requires-Dist: requests
39
- Requires-Dist: retrying (==1.3.4)
37
+ Requires-Dist: retrying (==1.4.2)
40
38
  Requires-Dist: tqdm
41
39
  Requires-Dist: typeguard (>=2,<3)
42
40
  Requires-Dist: typer