qwak-sdk 0.5.30__py3-none-any.whl → 0.5.32__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-sdk might be problematic. Click here for more details.

qwak_sdk/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  # fmt: off
2
2
  __author__ = '''Qwak.ai'''
3
- __version__ = '0.5.30'
3
+ __version__ = '0.5.32'
4
4
 
5
5
  from qwak.inner import wire_dependencies
6
6
 
File without changes
@@ -0,0 +1,140 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Optional
3
+
4
+ from _qwak_proto.qwak.feature_store.features.feature_set_pb2 import FeatureSet
5
+ from _qwak_proto.qwak.feature_store.features.feature_set_service_pb2 import (
6
+ GetFeatureSetByNameResponse,
7
+ )
8
+ from croniter import croniter
9
+ from qwak.clients.feature_store import FeatureRegistryClient
10
+ from qwak.feature_store.execution.backfill import Backfill
11
+ from qwak.feature_store.feature_sets.batch import BatchFeatureSetV1
12
+
13
+ from qwak_sdk.inner.tools.cli_tools import ask_yesno
14
+ from qwak_sdk.tools.colors import Color
15
+
16
+
17
+ class _BackfillLogic:
18
+ featureset: FeatureSet
19
+ batch_featureset: BatchFeatureSetV1
20
+
21
+ def configure_window_backfill(
22
+ self,
23
+ featureset_name: str,
24
+ start_time: Optional[datetime] = None,
25
+ stop_time: Optional[datetime] = None,
26
+ comment: str = None,
27
+ cluster_template: str = None,
28
+ ) -> Optional[Backfill]:
29
+ """
30
+ Configures a window back-fill object. This type of backfill will not reset the entire featureset, but will be
31
+ performed from the requested start time to the requested stop time in ticks calculated according to the
32
+ feature sets scheduling policy.
33
+ @param featureset_name: feature set name to perform the backfill job for
34
+ @type featureset_name: str
35
+ @param start_time: backfill requested start date
36
+ @type start_time: optional[datetime]
37
+ @param stop_time:
38
+ @type stop_time: optional[datetime]
39
+ @param comment:
40
+ @type comment: str
41
+ @param cluster_template:
42
+ @type cluster_template: str
43
+ @return:
44
+ @rtype:
45
+ """
46
+ print(
47
+ f"{Color.BLUE} Backfilling from {start_time} to {stop_time}, "
48
+ f"the following ticks are going to be performed {Color.END}"
49
+ )
50
+ zipped_tick_strs = Backfill.generate_expected_ticks_repr(
51
+ scheduling_policy=self.batch_featureset.scheduling_policy,
52
+ start_time=start_time,
53
+ stop_time=stop_time,
54
+ )
55
+ print("\n".join(zipped_tick_strs))
56
+ if ask_yesno("", force=False):
57
+ backfill_execution = Backfill(
58
+ featureset_name=featureset_name,
59
+ comment=comment,
60
+ cluster_template=cluster_template,
61
+ start_time=start_time,
62
+ stop_time=stop_time,
63
+ )
64
+ return backfill_execution
65
+ return None
66
+
67
+ @staticmethod
68
+ def _get_featureset_by_name(featureset_name: str) -> GetFeatureSetByNameResponse:
69
+ registry_client = FeatureRegistryClient()
70
+
71
+ return registry_client.get_feature_set_by_name(feature_set_name=featureset_name)
72
+
73
+ def get_start_stop_times(
74
+ self,
75
+ requested_start_time: Optional[datetime] = None,
76
+ requested_stop_time: Optional[datetime] = None,
77
+ ) -> tuple:
78
+ start_time: datetime = (
79
+ requested_start_time or self.batch_featureset.backfill.start_date
80
+ ).replace(tzinfo=timezone.utc)
81
+ stop_time: datetime
82
+
83
+ if start_time and start_time > datetime.now().replace(tzinfo=timezone.utc):
84
+ stop_time = datetime.utcnow().replace(tzinfo=timezone.utc)
85
+ else:
86
+ stop_time = (
87
+ requested_stop_time or datetime.utcnow().replace(tzinfo=timezone.utc)
88
+ ).replace(tzinfo=timezone.utc)
89
+
90
+ return start_time, stop_time
91
+
92
+ def is_valid_input(
93
+ self,
94
+ reset_backfill: bool,
95
+ start_time: Optional[datetime],
96
+ stop_time: Optional[datetime],
97
+ featureset_name: str,
98
+ ) -> bool:
99
+ # Validate reset_backfill or start and stop times
100
+ if (not reset_backfill and not start_time and not stop_time) or (
101
+ reset_backfill and (start_time or stop_time)
102
+ ):
103
+ print(
104
+ f"{Color.RED} please specify either --reset-backfill or one or more of --start-time/--stop-time"
105
+ )
106
+ return False
107
+
108
+ # Validate featureset exists
109
+ get_featureset_resp = self._get_featureset_by_name(
110
+ featureset_name=featureset_name
111
+ )
112
+ if not get_featureset_resp or not get_featureset_resp.feature_set:
113
+ print(f"{Color.RED} Failed to retrieve featureset {featureset_name}")
114
+ return False
115
+
116
+ self.featureset = get_featureset_resp.feature_set
117
+ fs_type_attr: str = self.featureset.feature_set_definition.feature_set_spec.feature_set_type.WhichOneof(
118
+ "set_type"
119
+ )
120
+ # Validate featureset is batch v1
121
+ if fs_type_attr != "batch_feature_set_v1":
122
+ print(
123
+ f"{Color.RED} Feature set {featureset_name} is of type {fs_type_attr}. Only BatchV1 is supported."
124
+ )
125
+ return False
126
+
127
+ self.batch_featureset = BatchFeatureSetV1.from_proto(
128
+ self.featureset.feature_set_definition.feature_set_spec
129
+ )
130
+
131
+ # Validate scheduling policy
132
+ if not self.batch_featureset.scheduling_policy or not croniter.is_valid(
133
+ self.batch_featureset.scheduling_policy
134
+ ):
135
+ print(
136
+ f"{Color.RED} Feature set {featureset_name} has an invalid scheduling policy {self.batch_featureset.scheduling_policy}."
137
+ )
138
+ return False
139
+
140
+ return True
@@ -0,0 +1,129 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import click
5
+ from qwak.feature_store.execution.backfill import Backfill
6
+
7
+ from qwak_sdk.commands.feature_store.backfill._logic import _BackfillLogic
8
+ from qwak_sdk.inner.tools.cli_tools import QwakCommand, ask_yesno
9
+ from qwak_sdk.tools.colors import Color
10
+
11
+
12
+ @click.command(
13
+ "backfill", cls=QwakCommand, help="Trigger a backfill process for a Feature Set"
14
+ )
15
+ @click.option(
16
+ "--feature-set",
17
+ "--name",
18
+ help="Feature Set name to perform the backfill process for",
19
+ required=True,
20
+ type=click.STRING,
21
+ )
22
+ @click.option(
23
+ "--reset-backfill",
24
+ "--reset",
25
+ is_flag=True,
26
+ metavar="FLAG",
27
+ default=False,
28
+ help="Perform a complete reset of the featuresets data. "
29
+ "This will result in the deletion of the current existing data.",
30
+ )
31
+ @click.option(
32
+ "--start-time",
33
+ type=click.DateTime(),
34
+ default=None,
35
+ required=False,
36
+ help="The time from which the featuresets data should be backfilled in UTC. "
37
+ "Defaults to the featuresets configured backfill start time.",
38
+ )
39
+ @click.option(
40
+ "--stop-time",
41
+ type=click.DateTime(),
42
+ default=None,
43
+ required=False,
44
+ help="The time up until the featuresets data should be backfilled in UTC. Defaults to the current timestamp. "
45
+ "If the time provided is in the future, stop time will be rounded down to the current time. ",
46
+ )
47
+ @click.option(
48
+ "--cluster-template",
49
+ type=str,
50
+ default=None,
51
+ required=False,
52
+ help="Backfill resource configuration, expects a ClusterType size. "
53
+ "Defaults to the featureset resource configuration",
54
+ )
55
+ @click.option(
56
+ "--comment",
57
+ type=str,
58
+ default=None,
59
+ required=False,
60
+ help="Backfill job optional comment tag line",
61
+ )
62
+ def backfill(
63
+ feature_set: str,
64
+ reset_backfill: bool,
65
+ start_time: Optional[datetime],
66
+ stop_time: Optional[datetime],
67
+ cluster_template: Optional[str],
68
+ comment: Optional[str],
69
+ **kwargs,
70
+ ):
71
+ backill_logic = _BackfillLogic()
72
+ if not backill_logic.is_valid_input(
73
+ reset_backfill=reset_backfill,
74
+ start_time=start_time,
75
+ stop_time=stop_time,
76
+ featureset_name=feature_set,
77
+ ):
78
+ return
79
+
80
+ if reset_backfill:
81
+ backfill_execution = _configure_backfill_triggered(
82
+ featureset_name=feature_set,
83
+ comment=comment,
84
+ cluster_template=cluster_template,
85
+ )
86
+ else:
87
+ start, stop = backill_logic.get_start_stop_times(
88
+ requested_start_time=start_time, requested_stop_time=stop_time
89
+ )
90
+ if start >= stop:
91
+ print(
92
+ f"{Color.RED} Stop time {stop.isoformat()} must be after start time {start.isoformat()}"
93
+ )
94
+ return
95
+
96
+ backfill_execution = backill_logic.configure_window_backfill(
97
+ featureset_name=feature_set,
98
+ start_time=start,
99
+ stop_time=stop,
100
+ comment=comment,
101
+ cluster_template=cluster_template,
102
+ )
103
+
104
+ if backfill_execution:
105
+ execution_id: str = backfill_execution.trigger_batch_backfill()
106
+ print(
107
+ f"{Color.BLUE}✅ Triggered backfill execution for featureset {feature_set}, "
108
+ f"execution id for follow up on is {execution_id}."
109
+ )
110
+ print(
111
+ f"To inquire the status of the execution run "
112
+ f"'qwak features execution-status --execution-id {execution_id}'"
113
+ )
114
+
115
+
116
+ def _configure_backfill_triggered(
117
+ featureset_name: str, comment: str, cluster_template: str
118
+ ) -> Optional[Backfill]:
119
+ if ask_yesno(
120
+ f"You are about to remove and re-populate all data in {featureset_name}",
121
+ force=False,
122
+ ):
123
+ print(f"{Color.RED} - A backfill reset was triggered")
124
+ return Backfill(
125
+ featureset_name=featureset_name,
126
+ comment=comment,
127
+ cluster_template=cluster_template,
128
+ )
129
+ return None
File without changes
@@ -0,0 +1,19 @@
1
+ import click
2
+ from qwak.feature_store.execution.execution_query import ExecutionQuery
3
+
4
+ from qwak_sdk.inner.tools.cli_tools import QwakCommand
5
+
6
+
7
+ @click.command(
8
+ "execution-status",
9
+ cls=QwakCommand,
10
+ help="Retrieve the current status of an execution (backfill/batch job ingestion)",
11
+ )
12
+ @click.option(
13
+ "--execution-id",
14
+ type=str,
15
+ required=True,
16
+ help="The id of the execution to retrieve the status for.",
17
+ )
18
+ def execution_status(execution_id: str, **kwargs):
19
+ print(ExecutionQuery.get_execution_status_message(execution_id=execution_id))
@@ -1,6 +1,8 @@
1
1
  import click
2
2
 
3
+ from qwak_sdk.commands.feature_store.backfill.ui import backfill
3
4
  from qwak_sdk.commands.feature_store.delete.ui import delete_fs_object
5
+ from qwak_sdk.commands.feature_store.execution.ui import execution_status
4
6
  from qwak_sdk.commands.feature_store.list.ui import list_feature_sets
5
7
  from qwak_sdk.commands.feature_store.pause.ui import pause_feature_set
6
8
  from qwak_sdk.commands.feature_store.register.ui import register_fs_objects
@@ -23,3 +25,5 @@ feature_store_commands_group.add_command(pause_feature_set)
23
25
  feature_store_commands_group.add_command(resume_feature_set)
24
26
  feature_store_commands_group.add_command(trigger_feature_set)
25
27
  feature_store_commands_group.add_command(register_fs_objects)
28
+ feature_store_commands_group.add_command(backfill)
29
+ feature_store_commands_group.add_command(execution_status)
@@ -29,6 +29,7 @@ from qwak_sdk.inner.tools.logger.logger import (
29
29
  BUILDS_LOGS = HOST_QWAK_HIDDEN_FOLDER / "logs" / "build"
30
30
  BUILD_LOG_NAME = "build.log"
31
31
  MAX_LOGS_NUMBER = 15
32
+ DEBUG_LEVEL = 2
32
33
 
33
34
 
34
35
  @contextlib.contextmanager
@@ -76,7 +77,7 @@ def setup_logger(
76
77
  set_handler_verbosity(
77
78
  logger,
78
79
  REMOTE_CONSOLE_HANDLER_NAME,
79
- VERBOSITY_LEVEL_MAPPING[verbosity_level],
80
+ VERBOSITY_LEVEL_MAPPING[DEBUG_LEVEL],
80
81
  )
81
82
 
82
83
  return logger
@@ -1,6 +1,6 @@
1
1
  from _qwak_proto.qwak.builds.builds_pb2 import BuildStatus
2
- from qwak.clients.build_management import BuildsManagementClient
2
+ from qwak.clients.build_orchestrator import BuildOrchestratorClient
3
3
 
4
4
 
5
5
  def execute_get_build_status(build_id) -> BuildStatus:
6
- return BuildsManagementClient().get_build(build_id).build.build_status
6
+ return BuildOrchestratorClient().get_build(build_id).build.build_status
@@ -1,7 +1,5 @@
1
- from copy import deepcopy
2
-
3
1
  from _qwak_proto.qwak.builds.builds_pb2 import SUCCESSFUL
4
- from qwak.clients.build_management.client import BuildsManagementClient
2
+ from qwak.clients.build_orchestrator import BuildOrchestratorClient
5
3
  from qwak.clients.model_management.client import ModelsManagementClient
6
4
  from qwak.exceptions import QwakException
7
5
 
@@ -20,12 +18,11 @@ def get_latest_successful_build_from_model(model_id: str) -> str:
20
18
  """
21
19
  model = ModelsManagementClient().get_model(model_id)
22
20
  model_uuid = model.uuid
23
- last_successful_build = BuildsManagementClient().list_builds(model_uuid)
24
- builds = deepcopy(last_successful_build.builds)
25
- builds = [build for build in builds if build.build_status == SUCCESSFUL]
26
- builds.sort(key=lambda r: r.created_at.seconds)
21
+ all_builds = BuildOrchestratorClient().list_builds(model_uuid).build
22
+ builds = [build for build in all_builds if build.build_status == SUCCESSFUL]
23
+ builds.sort(key=lambda r: r.audit.created_at.seconds)
27
24
 
28
25
  if not builds:
29
26
  raise QwakException(f"Unable to find successful build for model {model_id}")
30
27
 
31
- return builds[-1].build_spec.build_id
28
+ return builds[-1].buildId
@@ -1,3 +1,4 @@
1
+ from time import sleep
1
2
  from typing import Dict, List, Set
2
3
 
3
4
  from _qwak_proto.qwak.audience.v1.audience_pb2 import AudienceRoutesEntry
@@ -24,13 +25,19 @@ from qwak_sdk.commands.models.deployments.deploy._logic.deploy_config import (
24
25
  from qwak_sdk.commands.models.deployments.undeploy._logic.variations import (
25
26
  validate_variations_for_undeploy,
26
27
  )
28
+ from qwak_sdk.tools.utils import qwak_spinner
27
29
 
28
30
  NO_DEPLOYED_VARIATIONS_ERROR_MSG = (
29
31
  "There are currently no deployed variations for model {model_id} in {env_name}"
30
32
  )
31
-
33
+ UNDEPLOY_ERROR_MSG = "Environment {environment_id} failed with status: {status}"
32
34
  logger = get_qwak_logger()
33
35
 
36
+ FAILED_UNDEPLOYMENT_STATUS = ["FAILED_UNDEPLOYMENT"]
37
+ SUCCESSFUL_UNDEPLOYMENT_STATUS = ["SUCCESSFUL_UNDEPLOYMENT"]
38
+ END_UNDEPLOYMENT_STATUSES = SUCCESSFUL_UNDEPLOYMENT_STATUS + FAILED_UNDEPLOYMENT_STATUS
39
+ TIME_TO_WAIT_FOR_UNDEPLOYMENT_POLLING = 5
40
+
34
41
 
35
42
  def get_deployed_variation_name(existing_variations_names: Set[str]) -> str:
36
43
  return list(existing_variations_names)[0]
@@ -123,6 +130,7 @@ def undeploy(
123
130
  model_id: str,
124
131
  config: DeployConfig,
125
132
  model_uuid: str = "",
133
+ sync: bool = False,
126
134
  ):
127
135
  deployment_client = DeploymentManagementClient()
128
136
  ecosystem_client = EcosystemClient()
@@ -161,14 +169,108 @@ def undeploy(
161
169
  config.realtime.fallback_variation,
162
170
  )
163
171
 
172
+ environment_to_deployment_id = {}
173
+ if sync:
174
+ environment_to_deployment_id = __get_environment_to_deployment_id(
175
+ deployment_client, model_id, model_uuid, env_undeployment_requests
176
+ )
177
+
164
178
  undeployment_response = deployment_client.undeploy_model(
165
179
  model_id=model_id,
166
180
  model_uuid=model_uuid,
167
181
  env_undeployment_requests=env_undeployment_requests,
168
182
  )
169
183
 
184
+ if sync:
185
+ __sync_undeploy(
186
+ environment_to_deployment_id=environment_to_deployment_id,
187
+ model_id=model_id,
188
+ deployment_client=deployment_client,
189
+ )
170
190
  logger.info(
171
191
  f"Current status is {ModelDeploymentStatus.Name(undeployment_response.status)}."
172
192
  )
173
193
 
174
194
  return undeployment_response
195
+
196
+
197
+ def __get_environment_to_deployment_id(
198
+ deployment_client: DeploymentManagementClient,
199
+ model_id: str,
200
+ model_uuid: str,
201
+ env_undeployment_requests: Dict,
202
+ ) -> Dict:
203
+ deployment_details = deployment_client.get_deployment_details(
204
+ model_id, model_uuid
205
+ ).environment_to_deployment_details
206
+ result = {}
207
+ for env_id, env_deployment_details in env_undeployment_requests.items():
208
+ deployment_detail_by_env = deployment_details.get(env_id)
209
+ if deployment_detail_by_env:
210
+ for deployment_detail in deployment_detail_by_env.deployments_details:
211
+ if (
212
+ env_id == deployment_detail.environment_id
213
+ and deployment_detail.variation.name
214
+ == env_deployment_details.traffic_config.selected_variation_name
215
+ ):
216
+ result[env_id] = deployment_detail.deployment_id
217
+ return result
218
+
219
+
220
+ def __sync_undeploy(
221
+ environment_to_deployment_id: Dict,
222
+ model_id: str,
223
+ deployment_client: DeploymentManagementClient,
224
+ ):
225
+ with qwak_spinner(
226
+ begin_text=f"Undeploy - model: {model_id}",
227
+ end_text="Successful undeployment",
228
+ print_callback=print,
229
+ ):
230
+ for environment_id, deployment_id in environment_to_deployment_id.items():
231
+ status_result = _poll_undeployment_status(
232
+ deployment_id=deployment_id,
233
+ deployment_client=deployment_client,
234
+ check_every_n_seconds=TIME_TO_WAIT_FOR_UNDEPLOYMENT_POLLING,
235
+ )
236
+ failed_to_undeployment = []
237
+ for _, status in status_result.items():
238
+ if status in FAILED_UNDEPLOYMENT_STATUS:
239
+ failed_to_undeployment.append(
240
+ UNDEPLOY_ERROR_MSG.format(
241
+ environment_id=environment_id, status=status
242
+ )
243
+ )
244
+ if failed_to_undeployment:
245
+ raise QwakException("\n".join(failed_to_undeployment))
246
+
247
+
248
+ def _poll_undeployment_status(
249
+ deployment_id: str,
250
+ deployment_client: DeploymentManagementClient,
251
+ check_every_n_seconds: int,
252
+ ) -> Dict:
253
+ deployment_status = ""
254
+
255
+ while deployment_status not in END_UNDEPLOYMENT_STATUSES:
256
+ sleep(check_every_n_seconds)
257
+ deployment_status = __get_deployment_status(
258
+ deployment_id=deployment_id, deployment_client=deployment_client
259
+ )
260
+ return {deployment_id: deployment_status}
261
+
262
+
263
+ def __get_deployment_status(
264
+ deployment_id: str, deployment_client: DeploymentManagementClient
265
+ ):
266
+ try:
267
+ return ModelDeploymentStatus.Name(
268
+ deployment_client.get_deployment_status(
269
+ deployment_named_id=deployment_id,
270
+ ).status
271
+ )
272
+ except QwakException as e:
273
+ logger.error(
274
+ f"Got error while trying to get deployment id: {deployment_id} status. Error is: {e.message}"
275
+ )
276
+ return ""
@@ -42,11 +42,18 @@ logger = get_qwak_logger()
42
42
  required=False,
43
43
  type=click.Path(exists=True, resolve_path=True, dir_okay=False),
44
44
  )
45
+ @click.option(
46
+ "--sync",
47
+ is_flag=True,
48
+ default=False,
49
+ help="Waiting for deployments to be undeploy",
50
+ )
45
51
  def models_undeploy(
46
52
  model_id: str,
47
53
  variation_name: str,
48
54
  environment_name: List[str],
49
55
  from_file: str = None,
56
+ sync: bool = False,
50
57
  **kwargs,
51
58
  ):
52
59
  logger.info(f"Initiating undeployment for model '{model_id}'")
@@ -62,8 +69,4 @@ def models_undeploy(
62
69
  )
63
70
  model_uuid = models_management.get_model_uuid(model_id)
64
71
 
65
- undeploy(
66
- model_id=model_id,
67
- config=config,
68
- model_uuid=model_uuid,
69
- )
72
+ undeploy(model_id=model_id, config=config, model_uuid=model_uuid, sync=sync)
@@ -4,20 +4,27 @@ from datetime import datetime
4
4
  from _qwak_proto.qwak.builds.builds_pb2 import BuildStatus, ValueType
5
5
  from _qwak_proto.qwak.deployment.deployment_pb2 import ModelDeploymentStatus
6
6
  from google.protobuf.json_format import MessageToDict
7
- from qwak.clients.build_management import BuildsManagementClient
7
+ from qwak.clients.build_orchestrator import BuildOrchestratorClient
8
8
  from qwak.clients.deployment.client import DeploymentManagementClient
9
9
  from qwak.clients.model_management import ModelsManagementClient
10
10
  from tabulate import tabulate
11
11
 
12
12
 
13
- def execute_model_describe(model_id, interface, show_list_builds, format):
13
+ def execute_model_describe(model_id, interface, show_list_builds, output_format):
14
14
  model = _model_data(model_id)
15
+ deployment_data = _get_deployment_data(model)
15
16
  list_builds = _builds_data(show_list_builds, model)
16
- interface_build = _interface_data(interface, model)
17
- if format == "text":
18
- print_text_data(model, list_builds, interface_build)
19
- elif format == "json":
20
- print_json_data(model, list_builds, interface_build)
17
+ schema_data = _schema_data(interface, deployment_data)
18
+ if output_format == "text":
19
+ print_text_data(model, list_builds, schema_data, deployment_data)
20
+ elif output_format == "json":
21
+ print_json_data(model, list_builds, schema_data)
22
+
23
+
24
+ def _get_deployment_data(model):
25
+ return DeploymentManagementClient().get_deployment_details(
26
+ model_id=model.model_id, model_uuid=model.uuid
27
+ )
21
28
 
22
29
 
23
30
  def _model_data(model_id):
@@ -27,128 +34,136 @@ def _model_data(model_id):
27
34
 
28
35
  def _builds_data(list_builds, model):
29
36
  if list_builds:
30
- builds_management = BuildsManagementClient()
31
- return builds_management.list_builds(model.uuid)
37
+ builds_orchestrator = BuildOrchestratorClient()
38
+ return builds_orchestrator.list_builds(model.uuid)
32
39
  return None
33
40
 
34
41
 
35
- def _interface_data(interface, model):
36
- if interface:
37
- deployment_client = DeploymentManagementClient()
38
- deployment_details = deployment_client.get_deployment_details(
39
- model_id=model.model_id, model_uuid=model.uuid
42
+ def _schema_data(interface, deployment_data):
43
+ if interface and deployment_data.current_deployment_details.build_id:
44
+ build_response = BuildOrchestratorClient().get_build(
45
+ deployment_data.current_deployment_details.build_id
40
46
  )
41
- if deployment_details.current_deployment_details.build_id:
42
- builds_management = BuildsManagementClient()
43
- build_response = builds_management.get_build(
44
- deployment_details.current_deployment_details.build_id
45
- )
46
- if build_response.build.HasField("model_schema"):
47
- return build_response
47
+ if build_response.build.HasField("model_schema"):
48
+ return build_response.build.model_schema
48
49
  return None
49
50
 
50
51
 
51
- def print_json_data(model, list_builds, interface_build):
52
+ def print_json_data(model, list_builds, schema_data):
52
53
  output = MessageToDict(model)
53
54
  if list_builds:
54
55
  output["builds"] = MessageToDict(list_builds)
55
- if interface_build:
56
- output["interface"] = MessageToDict(interface_build)["build"]["modelSchema"]
56
+ if schema_data:
57
+ output["interface"] = MessageToDict(schema_data)
57
58
  print(json.dumps(output, indent=4, sort_keys=True))
58
59
 
59
60
 
60
- def print_text_data(model, list_builds, interface_build):
61
+ def print_text_data(model, list_builds, schema_data, deployment_data):
61
62
  print(
62
63
  f"Model id: {model.model_id}\nDisplay name: {model.display_name}\nDescription: {model.model_description}\n"
63
64
  + f'Creation Date: {datetime.fromtimestamp(model.created_at.seconds + model.created_at.nanos / 1e9).strftime("%A, %B %d, %Y %I:%M:%S")}\n'
64
65
  + f'Last update: {datetime.fromtimestamp(model.created_at.seconds + model.last_modified_at.nanos / 1e9).strftime("%A, %B %d, %Y %I:%M:%S")}'
65
66
  )
66
67
  if list_builds:
67
- columns = [
68
- "Build id",
69
- "Commit id",
70
- "Last modified date",
71
- "Build Status",
72
- "Deployment build status",
68
+ _print_text_builds(deployment_data, list_builds)
69
+ if schema_data:
70
+ _print_text_schema(schema_data)
71
+
72
+
73
+ def _print_text_schema(schema_data):
74
+ columns = [
75
+ "Parameter name",
76
+ "Parameter type",
77
+ "Parameter source",
78
+ "Parameter category",
79
+ ]
80
+ data = []
81
+ for entity in schema_data.entities:
82
+ data.append(
83
+ [
84
+ entity.name,
85
+ ValueType.Types.Name(entity.type.type),
86
+ None,
87
+ "Input",
88
+ ]
89
+ )
90
+ for feature in schema_data.features:
91
+ parsed_feature = _parse_feature(feature)
92
+ if parsed_feature:
93
+ data.append(parsed_feature)
94
+
95
+ for prediction in schema_data.predictions:
96
+ data.append(
97
+ [
98
+ prediction.name,
99
+ ValueType.Types.Name(prediction.type.type),
100
+ None,
101
+ "Output",
102
+ ]
103
+ )
104
+ print("\n" + tabulate(data, headers=columns))
105
+
106
+
107
+ def _parse_feature(feature):
108
+ if feature.HasField("explicit_feature"):
109
+ return [
110
+ feature.explicit_feature.name,
111
+ ValueType.Types.Name(feature.explicit_feature.type.type),
112
+ None,
113
+ "Input",
73
114
  ]
74
- data = []
75
- for build in list_builds.builds:
76
- deployment_status = (
77
- ModelDeploymentStatus.Name(number=build.deployment_build_status)
78
- if build.deployment_build_status != 0
79
- else ""
80
- )
81
- data.append(
82
- [
83
- build.build_spec.build_id,
84
- build.build_spec.commit_id,
85
- datetime.fromtimestamp(
86
- build.last_modified_at.seconds
87
- + build.last_modified_at.nanos / 1e9
88
- ).strftime("%A, %B %d, %Y %I:%M:%S"),
89
- BuildStatus.Name(number=build.build_status),
90
- deployment_status,
91
- ]
92
- )
93
- print("\n" + tabulate(data, headers=columns))
94
- if interface_build:
95
- model_schema = interface_build.build.model_schema
96
- columns = [
97
- "Parameter name",
98
- "Parameter type",
99
- "Parameter source",
100
- "Parameter category",
115
+ elif feature.HasField("batch_feature"):
116
+ return [
117
+ feature.batch_feature.name,
118
+ None,
119
+ feature.batch_feature.entity.name,
120
+ "Batch Feature",
101
121
  ]
102
- data = []
103
- for entity in model_schema.entities:
104
- data.append(
105
- [
106
- entity.name,
107
- ValueType.Types.Name(entity.type.type),
108
- None,
109
- "Input",
110
- ]
111
- )
112
- for feature in model_schema.features:
113
- if feature.HasField("explicit_feature"):
114
- data.append(
115
- [
116
- feature.explicit_feature.name,
117
- ValueType.Types.Name(feature.explicit_feature.type.type),
118
- None,
119
- "Input",
120
- ]
121
- )
122
- elif feature.HasField("batch_feature"):
123
- data.append(
124
- [
125
- feature.batch_feature.name,
126
- None,
127
- feature.batch_feature.entity.name,
128
- "Batch Feature",
129
- ]
130
- )
131
- elif feature.HasField("on_the_fly_feature"):
132
- data.append(
133
- [
134
- feature.on_the_fly_feature.name,
135
- None,
136
- str(
137
- [
138
- source.explicit_feature.name
139
- for source in feature.on_the_fly_feature.source_features
140
- ]
141
- ),
142
- "On-The-Fly Feature",
143
- ]
144
- )
145
- for prediction in model_schema.predictions:
146
- data.append(
122
+ elif feature.HasField("on_the_fly_feature"):
123
+ return [
124
+ feature.on_the_fly_feature.name,
125
+ None,
126
+ str(
147
127
  [
148
- prediction.name,
149
- ValueType.Types.Name(prediction.type.type),
150
- None,
151
- "Output",
128
+ source.explicit_feature.name
129
+ for source in feature.on_the_fly_feature.source_features
152
130
  ]
131
+ ),
132
+ "On-The-Fly Feature",
133
+ ]
134
+
135
+
136
+ def _print_text_builds(deployment_data, list_builds):
137
+ columns = [
138
+ "Build id",
139
+ "Commit id",
140
+ "Last modified date",
141
+ "Build Status",
142
+ "Deployment build status",
143
+ ]
144
+ data = []
145
+ for build in list_builds.build:
146
+ deployments = deployment_data.build_to_environment_deployment_status.get(
147
+ build.buildId
148
+ )
149
+ if deployments:
150
+ deployment_status = ModelDeploymentStatus.Name(
151
+ number=list(deployments.environment_to_deployment_brief.values())[
152
+ 0
153
+ ].status
153
154
  )
154
- print("\n" + tabulate(data, headers=columns))
155
+ else:
156
+ deployment_status = ""
157
+ data.append(
158
+ [
159
+ build.buildId,
160
+ build.commitId,
161
+ datetime.fromtimestamp(
162
+ build.audit.last_modified_at.seconds
163
+ + build.audit.last_modified_at.nanos / 1e9
164
+ ).strftime("%A, %B %d, %Y %I:%M:%S"),
165
+ BuildStatus.Name(number=build.build_status),
166
+ deployment_status,
167
+ ]
168
+ )
169
+ print("\n" + tabulate(data, headers=columns))
@@ -2,7 +2,7 @@ import time
2
2
  from datetime import datetime, timedelta, timezone
3
3
 
4
4
  from _qwak_proto.qwak.builds.builds_pb2 import BuildStatus
5
- from qwak.clients.build_management import BuildsManagementClient
5
+ from qwak.clients.build_orchestrator import BuildOrchestratorClient
6
6
  from qwak.clients.logging_client import LoggingClient
7
7
  from qwak.exceptions import QwakException
8
8
 
@@ -31,6 +31,10 @@ class QwakLogHandling:
31
31
  def __init__(self):
32
32
  self.log_formatter = self.LogFormatting()
33
33
 
34
+ @staticmethod
35
+ def get_build_status_name(build_client, build_id: str):
36
+ return BuildStatus.Name(build_client.get_build(build_id).build.build_status)
37
+
34
38
  def get_logs(
35
39
  self, follow, since, number_of_results, grep, source_params, source_name
36
40
  ):
@@ -39,7 +43,7 @@ class QwakLogHandling:
39
43
 
40
44
  logging_client = LoggingClient()
41
45
  if source_name == "build":
42
- build_client = BuildsManagementClient()
46
+ build_client = BuildOrchestratorClient()
43
47
  while True:
44
48
  params = {
45
49
  **source_params,
@@ -65,10 +69,8 @@ class QwakLogHandling:
65
69
  after = response.last_offset
66
70
  elif follow:
67
71
  if source_name == "build":
68
- build_status = BuildStatus.Name(
69
- build_client.get_build(
70
- source_params["build_id"]
71
- ).build.build_status
72
+ build_status = self.get_build_status_name(
73
+ build_client, source_params["build_id"]
72
74
  )
73
75
  if build_status in self.BUILD_FINISHED_STATUS:
74
76
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qwak-sdk
3
- Version: 0.5.30
3
+ Version: 0.5.32
4
4
  Summary: Qwak SDK and CLI for qwak models
5
5
  License: Apache-2.0
6
6
  Keywords: mlops,ml,deployment,serving,model
@@ -21,13 +21,14 @@ Provides-Extra: batch
21
21
  Provides-Extra: feedback
22
22
  Requires-Dist: boto3 (>=1.24.116,<2.0.0) ; extra == "batch" or extra == "feedback"
23
23
  Requires-Dist: cookiecutter
24
+ Requires-Dist: croniter (==1.4.1)
24
25
  Requires-Dist: gitpython (>=2.1.0)
25
26
  Requires-Dist: joblib (>=1.1.0,<2.0.0) ; extra == "batch" or extra == "feedback"
26
27
  Requires-Dist: pandas (<1.4) ; (python_full_version >= "3.7.1" and python_version < "3.8") and (extra == "batch" or extra == "feedback")
27
28
  Requires-Dist: pandas (>=1.4.3,<2.0.0) ; (python_version >= "3.8" and python_version < "3.10") and (extra == "batch" or extra == "feedback")
28
29
  Requires-Dist: pyarrow (>=6.0.0,<11.0.0) ; extra == "batch"
29
30
  Requires-Dist: python-json-logger (>=2.0.2)
30
- Requires-Dist: qwak-core (==0.1.12)
31
+ Requires-Dist: qwak-core (==0.1.36)
31
32
  Requires-Dist: qwak-inference (==0.1.8)
32
33
  Requires-Dist: tabulate (>=0.8.0)
33
34
  Requires-Dist: yaspin (>=2.0.0)
@@ -1,4 +1,4 @@
1
- qwak_sdk/__init__.py,sha256=5odqJ75r9h7i6Y6Vu1DonFXIzg5vR62Y62zBMSNFmNg,135
1
+ qwak_sdk/__init__.py,sha256=huJCMSFUcwVASX5p43L2I3GPEx5O4EKNrTDBkQKeuI4,135
2
2
  qwak_sdk/cli.py,sha256=FIK1dUNxR57ypb-CeD7fKSJnPJ02lrjR9G4aj2qMLPU,2458
3
3
  qwak_sdk/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  qwak_sdk/commands/_logic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -79,10 +79,15 @@ qwak_sdk/commands/automations/register/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JC
79
79
  qwak_sdk/commands/automations/register/_logic.py,sha256=C4ghMOKMNLlSQD1ZEb508VJHvuoSfy0mc2vjNQNFyz4,1624
80
80
  qwak_sdk/commands/automations/register/ui.py,sha256=nRfLwJ6dxEjNjFd4guvkT2v_LBC2r0t0gsh6NqZUTH8,1331
81
81
  qwak_sdk/commands/feature_store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
+ qwak_sdk/commands/feature_store/backfill/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
+ qwak_sdk/commands/feature_store/backfill/_logic.py,sha256=wuCq7Qq2OAFxiz3XeODRmQStc1ZFgFLo1b0hPRfCDXU,5330
84
+ qwak_sdk/commands/feature_store/backfill/ui.py,sha256=tcTSWMJ9HiA16fWFtfTABfSUvTTFQLnwkpDJTHxddDY,3934
82
85
  qwak_sdk/commands/feature_store/delete/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
86
  qwak_sdk/commands/feature_store/delete/_logic.py,sha256=KVNoVCBuh2X67GlfeSYjK-NrbYSajtB-d0W46LnbPUg,1716
84
87
  qwak_sdk/commands/feature_store/delete/ui.py,sha256=-LyDBnrHesFaXqUfuhnh_GAyWRdhsmDnqmkGv1DUESM,1039
85
- qwak_sdk/commands/feature_store/feature_store_command_group.py,sha256=1sWEiZpgtr4j2y5m2ouSJveG-B2tSAzWH0XMGhJzAuk,1002
88
+ qwak_sdk/commands/feature_store/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
+ qwak_sdk/commands/feature_store/execution/ui.py,sha256=7lZHUIjsLjxg0L9oIX4DqoVPKi0i0iYy7KHXCoZtLto,562
90
+ qwak_sdk/commands/feature_store/feature_store_command_group.py,sha256=Ooi6vvKZf5D4AwPY4b3gdd-njIuQYwlGjvt7LI-T6SM,1251
86
91
  qwak_sdk/commands/feature_store/list/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
92
  qwak_sdk/commands/feature_store/list/ui.py,sha256=NuXnQ3cdXSDCFAcC2jmgx4sAdjNuyIQa18h54Dflji8,4145
88
93
  qwak_sdk/commands/feature_store/pause/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -103,7 +108,7 @@ qwak_sdk/commands/models/build/_logic/build_steps.py,sha256=K03FIlA67Cq8dpRf-u8J
103
108
  qwak_sdk/commands/models/build/_logic/client_logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
109
  qwak_sdk/commands/models/build/_logic/client_logs/build_run_handlers.py,sha256=f7KQwdRNW8J_T9KMMaSbSpMRZFY-uqs82yxwJcg2_PE,6185
105
110
  qwak_sdk/commands/models/build/_logic/client_logs/cli_ui.py,sha256=nyQ-L6Q_C_g4Tb0rnBy0UCXnhJt-cwiuxDyQ28RnIoE,4647
106
- qwak_sdk/commands/models/build/_logic/client_logs/logger.py,sha256=SPyqJXGVcPZiyAmxOcSOhJbNhQYq7KjkIbFLGkR8YNE,2908
111
+ qwak_sdk/commands/models/build/_logic/client_logs/logger.py,sha256=ukah6zSZT9YJeSnlkNyTq1ZmLUl4zenUzm5e1XdVqRA,2920
107
112
  qwak_sdk/commands/models/build/_logic/client_logs/messages.py,sha256=1zcvDzt7dcNKnMA1YyNfkDWFZs7MfLavflZN5RrBLlk,1369
108
113
  qwak_sdk/commands/models/build/_logic/client_logs/notifier_impl.py,sha256=TLAvkmiMT-TQm0Tu_P0Cy-WQ6nH-NYpvNauT8DNo5uQ,1435
109
114
  qwak_sdk/commands/models/build/_logic/client_logs/spinner.py,sha256=iph1eVC7QtSzNobNP2j_x0DC2k7bbka9eT8rcBpZGeQ,359
@@ -155,7 +160,7 @@ qwak_sdk/commands/models/builds/cancel/ui.py,sha256=wmtGv55ORwju-uXjNLYBb8vedAQ3
155
160
  qwak_sdk/commands/models/builds/logs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
156
161
  qwak_sdk/commands/models/builds/logs/ui.py,sha256=Z2gNoJjg7YQX3e2qhXq3DMu605tr-vkaYqzVro0W6ok,1054
157
162
  qwak_sdk/commands/models/builds/status/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
158
- qwak_sdk/commands/models/builds/status/_logic.py,sha256=uBuB1uMHC7FLfF20ETqPfo4A_veJHLSzb6tgCCS75V0,256
163
+ qwak_sdk/commands/models/builds/status/_logic.py,sha256=B3AUKqKY4hHYb6vlfJLZy90DP86inxPZsmj860YTFvs,260
159
164
  qwak_sdk/commands/models/builds/status/ui.py,sha256=zR7TPiVRkWoN1NEoCbyuPCIyB2BMfjVbGMdBVXPBh70,1209
160
165
  qwak_sdk/commands/models/create/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
161
166
  qwak_sdk/commands/models/create/_logic.py,sha256=9t4OLQB8MKji7yIzHbvyPNAV7Ek7OaRt0AP3q0PUvlM,1335
@@ -173,7 +178,7 @@ qwak_sdk/commands/models/deployments/deploy/_logic/deployment.py,sha256=zBTjeeEV
173
178
  qwak_sdk/commands/models/deployments/deploy/_logic/deployment_message_helpers.py,sha256=72T3QV_ZeVgvVJ9lnjoLBjW4Hs77NUlVbyoltTWbspk,4516
174
179
  qwak_sdk/commands/models/deployments/deploy/_logic/deployment_response_handler.py,sha256=cHa2iF_2A8e1wx2a4UZuTjQ5IHgPvua1xziFJwzP_6s,5837
175
180
  qwak_sdk/commands/models/deployments/deploy/_logic/deployment_size_mapper.py,sha256=OoRGjcBM6jZYPr4mHPcefJzBH0bCXfu5QPlqLAsx2Ws,2160
176
- qwak_sdk/commands/models/deployments/deploy/_logic/get_latest_successful_build.py,sha256=NVHjRVM2oQhlJxMYXD1yQ5ZT6Jcd5msO1hpzGdrC64I,1067
181
+ qwak_sdk/commands/models/deployments/deploy/_logic/get_latest_successful_build.py,sha256=fsahuSvgGzVjkZGaTH9YyNSOtRWBHYlyAU3wTriyCgI,978
177
182
  qwak_sdk/commands/models/deployments/deploy/_logic/variations.py,sha256=n3OGHTYgjnmLYbafZbAOaudaGbeKjw8jbX4MhZX47fE,3256
178
183
  qwak_sdk/commands/models/deployments/deploy/batch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
184
  qwak_sdk/commands/models/deployments/deploy/batch/_logic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -194,11 +199,11 @@ qwak_sdk/commands/models/deployments/deploy/streaming/_logic/serving_strategy_ma
194
199
  qwak_sdk/commands/models/deployments/deploy/streaming/ui.py,sha256=9tPMnVnLNX3lYyBd4BAh-YThDFz99oyvrCRd_tDZufU,5796
195
200
  qwak_sdk/commands/models/deployments/undeploy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
196
201
  qwak_sdk/commands/models/deployments/undeploy/_logic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
- qwak_sdk/commands/models/deployments/undeploy/_logic/request_undeploy.py,sha256=8DMxVBFHa_Hf_iC-RwHZaCoiP3DEXvMeUP1Ah42zuZY,5677
202
+ qwak_sdk/commands/models/deployments/undeploy/_logic/request_undeploy.py,sha256=e-9lHQ2AbTyogUHxppPVli5pugl6ml4c1zAuFoLQTpw,9481
198
203
  qwak_sdk/commands/models/deployments/undeploy/_logic/variations.py,sha256=qo4qUB78voUhtM1YIuTz1OSBaLRS3xw97bwxgqRuLgI,3036
199
- qwak_sdk/commands/models/deployments/undeploy/ui.py,sha256=NDEir1x0s44r2iF_4hSe-lC7pXmHBYyZJ0Sr1cmV8Nk,1885
204
+ qwak_sdk/commands/models/deployments/undeploy/ui.py,sha256=1XYBouPgbJWOJCNR1UU1UOVnQS10iKsUqeNkvIBhHY8,2008
200
205
  qwak_sdk/commands/models/describe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
201
- qwak_sdk/commands/models/describe/_logic.py,sha256=FjjFuK4Q5HcZEEG7FzV-8fxlAXaCts2w8psn7jwTYnE,5781
206
+ qwak_sdk/commands/models/describe/_logic.py,sha256=LN1r3zrXLHf83U5ate7ED7VdoZuWUqcHwLZPMMZXRfs,5589
202
207
  qwak_sdk/commands/models/describe/ui.py,sha256=6QHt_EQRe5yRV9e2yYOAROfqU-_BfmS0VHXP4hfY6D8,968
203
208
  qwak_sdk/commands/models/executions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
204
209
  qwak_sdk/commands/models/executions/cancel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -354,9 +359,9 @@ qwak_sdk/main.py,sha256=KmNxbBxJQtKllX9mmA1jeUXh9dXVzq4W-qfHAHatcvA,136
354
359
  qwak_sdk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
355
360
  qwak_sdk/tools/colors.py,sha256=7pui_GGjC4uZKYFsIyXaJjYsjLxJVHb4OrfTgr93hqo,287
356
361
  qwak_sdk/tools/files.py,sha256=AyKJTOy7NhvP3SrqwIw_lxYNCOy1CvLgMmSJpWZ0OKM,2257
357
- qwak_sdk/tools/log_handling.py,sha256=CC9XC3z3eHr_fehQgDsXZr4VP29VaMRwC05xvC5Wue4,5616
362
+ qwak_sdk/tools/log_handling.py,sha256=NIQ-5uCb2NHzB7KwIbqbR9EhmhL1bawXLD_oamB6Gok,5711
358
363
  qwak_sdk/tools/utils.py,sha256=SHmU4r_m2ABZyFYMC03P17GvltPbYbmB39hvalIZEtI,1168
359
- qwak_sdk-0.5.30.dist-info/entry_points.txt,sha256=vSl0ELYDyj640oMM57u0AjBP87wtLYxCcGOendhEx80,47
360
- qwak_sdk-0.5.30.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
361
- qwak_sdk-0.5.30.dist-info/METADATA,sha256=_dYHtPZ8myXwOX711Oh0FnfkfK8Yp-WI19eQnUVZGc0,1851
362
- qwak_sdk-0.5.30.dist-info/RECORD,,
364
+ qwak_sdk-0.5.32.dist-info/entry_points.txt,sha256=vSl0ELYDyj640oMM57u0AjBP87wtLYxCcGOendhEx80,47
365
+ qwak_sdk-0.5.32.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
366
+ qwak_sdk-0.5.32.dist-info/METADATA,sha256=tpM7nE5JSP8lkm_GJuMeSvSd5YQt8VEs2Nsgse0jxbk,1885
367
+ qwak_sdk-0.5.32.dist-info/RECORD,,