wizata-dsapi 2.2.0.dev24__tar.gz → 2.2.0.dev25__tar.gz

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.
Files changed (76) hide show
  1. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/__init__.py +3 -1
  3. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/context.py +3 -0
  4. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/pipeline.py +85 -1
  5. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/pipeline_image.py +52 -29
  6. wizata_dsapi-2.2.0.dev25/wizata_dsapi/python_package.py +222 -0
  7. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/script.py +7 -145
  8. wizata_dsapi-2.2.0.dev25/wizata_dsapi/version.py +1 -0
  9. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizata_dsapi_client.py +103 -2
  10. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  11. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/SOURCES.txt +1 -0
  12. wizata_dsapi-2.2.0.dev24/wizata_dsapi/version.py +0 -1
  13. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/LICENSE.txt +0 -0
  14. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/README.rst +0 -0
  15. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/setup.cfg +0 -0
  16. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/setup.py +0 -0
  17. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_config.py +0 -0
  18. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_dto.py +0 -0
  19. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_interface.py +0 -0
  20. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/bucket.py +0 -0
  21. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/business_label.py +0 -0
  22. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection.py +0 -0
  23. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_discovery.py +0 -0
  24. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_policy.py +0 -0
  25. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_types.py +0 -0
  26. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dashboard.py +0 -0
  27. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dataframe_toolkit.py +0 -0
  28. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/datapoint.py +0 -0
  29. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/datastore.py +0 -0
  30. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/deployment.py +0 -0
  31. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/ds_dataframe.py +0 -0
  32. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  33. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_config.py +0 -0
  34. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_device.py +0 -0
  35. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_module.py +0 -0
  36. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_package.py +0 -0
  37. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/evaluation.py +0 -0
  38. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/execution.py +0 -0
  39. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/execution_log.py +0 -0
  40. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/experiment.py +0 -0
  41. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/formula.py +0 -0
  42. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/graylog_log.py +0 -0
  43. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/group_system.py +0 -0
  44. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/ilogger.py +0 -0
  45. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/insight.py +0 -0
  46. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/inventory.py +0 -0
  47. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/mlmodel.py +0 -0
  48. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/mobile_asset.py +0 -0
  49. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/model_toolkit.py +0 -0
  50. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/models/__init__.py +0 -0
  51. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/models/common.py +0 -0
  52. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/notification.py +0 -0
  53. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/paged_query_result.py +0 -0
  54. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plot.py +0 -0
  55. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/__init__.py +0 -0
  56. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/common.py +0 -0
  57. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/theme.py +0 -0
  58. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/product.py +0 -0
  59. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/request.py +0 -0
  60. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/scripts/__init__.py +0 -0
  61. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/scripts/common.py +0 -0
  62. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/search.py +0 -0
  63. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/solution_component.py +0 -0
  64. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/streamlit_utils.py +0 -0
  65. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/template.py +0 -0
  66. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/template_config.py +0 -0
  67. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/trigger.py +0 -0
  68. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/twin.py +0 -0
  69. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/twinregistration.py +0 -0
  70. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/user.py +0 -0
  71. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizard_function.py +0 -0
  72. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizard_request.py +0 -0
  73. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/words.py +0 -0
  74. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  75. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/requires.txt +0 -0
  76. {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev24
3
+ Version: 2.2.0.dev25
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -11,7 +11,9 @@ from .execution import Execution, ExecutionStatus, ExecutionStepLog, AbortedExce
11
11
  from .execution_log import ExecutionLog
12
12
  from .experiment import Experiment
13
13
  from .ds_dataframe import DSDataFrame
14
- from .script import Script, ScriptConfig, ScriptType, ScriptCategory, ScriptLibrary, sanitize_blob_name, discover_from_wheel
14
+ from .script import Script, ScriptConfig, ScriptType, ScriptCategory, sanitize_blob_name
15
+ from .python_package import (PythonPackage, PythonPackageVersion, PythonPackageSource, PythonPackagePin,
16
+ normalize_python_package_name)
15
17
  from .template import Template, TemplateProperty
16
18
  from .solution_component import SolutionComponent, SolutionType
17
19
  from .business_label import BusinessLabel
@@ -77,6 +77,9 @@ class Context(ILogger):
77
77
  # version — not the Postgres "current" one. Left None off the git path.
78
78
  self.branch = None
79
79
  self.sha = None
80
+ # DEV-6120: the Python packages mounted for the run, [{name, version, sha256}];
81
+ # a model trained in it records them.
82
+ self.python_packages = []
80
83
  self.template = template
81
84
  self.registration = registration
82
85
  self.now = now
@@ -14,6 +14,7 @@ from .twin import Twin
14
14
  from .twinregistration import TwinRegistration, TwinRegistrationProperty
15
15
  from .datapoint import DataPoint
16
16
  from .formula import normalize_formulas
17
+ from .python_package import PythonPackagePin, normalize_python_package_name
17
18
 
18
19
  from enum import Enum
19
20
 
@@ -592,6 +593,9 @@ class Pipeline(ApiDto):
592
593
  :ivar list steps: list of steps connected between by their inputs and outputs.
593
594
  :ivar uuid.UUID template_id: template id associated to the pipeline.
594
595
  :ivar dict variables: dictionary of variable name with they wizata_dsapi.VarType.
596
+ :ivar list python_packages: the Python packages the pipeline imports, as
597
+ ``PythonPackagePin`` (JSON ``pythonPackages``): an uploaded package with its version,
598
+ a git package by name only.
595
599
  :ivar uuid.UUID createdById: unique identifier of creating user.
596
600
  :ivar int createdDate: timestamp of created date.
597
601
  :ivar uuid.UUID updatedById: unique identifier of updating user.
@@ -615,7 +619,8 @@ class Pipeline(ApiDto):
615
619
  steps: list = None,
616
620
  plots: list = None,
617
621
  template_id: uuid.UUID = None,
618
- experiment_id: uuid.UUID = None):
622
+ experiment_id: uuid.UUID = None,
623
+ python_packages: list = None):
619
624
 
620
625
  if pipeline_id is None:
621
626
  pipeline_id = uuid.uuid4()
@@ -637,6 +642,10 @@ class Pipeline(ApiDto):
637
642
  self.plots = plots
638
643
  if plots is None:
639
644
  self.plots = []
645
+ self._python_packages = list(python_packages) if python_packages is not None else []
646
+ # DEV-6120: whether the list was given (constructor, JSON key, assignment). A save
647
+ # that leaves it out keeps the stored list - the 12.2 editor does not know it.
648
+ self._python_packages_given = python_packages is not None
640
649
  if steps is not None:
641
650
  for step in steps:
642
651
  if not isinstance(step, PipelineStep):
@@ -663,11 +672,75 @@ class Pipeline(ApiDto):
663
672
  def key(self):
664
673
  del self._key
665
674
 
675
+ @property
676
+ def python_packages(self) -> list:
677
+ """the Python packages the pipeline imports, as ``PythonPackagePin`` (DEV-6120)."""
678
+ return self._python_packages
679
+
680
+ @python_packages.setter
681
+ def python_packages(self, value):
682
+ self._python_packages = list(value) if value is not None else []
683
+ self._python_packages_given = True
684
+
685
+ @property
686
+ def python_packages_given(self) -> bool:
687
+ """
688
+ whether ``python_packages`` was set (constructor, ``pythonPackages`` in the JSON, or
689
+ an assignment). An update that leaves it out keeps the stored list; ``[]`` clears it.
690
+ """
691
+ return self._python_packages_given
692
+
693
+ def add_python_package(self, name: str, version: str = None) -> PythonPackagePin:
694
+ """
695
+ import a Python package in this pipeline (DEV-6120). Adding a package already listed
696
+ replaces its pin.
697
+ :param name: the package's name.
698
+ :param version: the uploaded version to run with; None for a git package.
699
+ """
700
+ pin = PythonPackagePin(name=name, version=version)
701
+ normalized = normalize_python_package_name(name)
702
+ self.python_packages = [p for p in self.python_packages
703
+ if not isinstance(p.name, str)
704
+ or normalize_python_package_name(p.name) != normalized]
705
+ self.python_packages.append(pin)
706
+ return pin
707
+
708
+ def check_python_packages(self) -> bool:
709
+ """
710
+ validate the shape of ``python_packages``: a name each, a version that is text when
711
+ given, no package twice. Whether the package and version exist is checked on save.
712
+ """
713
+ seen = set()
714
+ for pin in self.python_packages:
715
+ if not isinstance(pin, PythonPackagePin):
716
+ raise ValueError(f"pythonPackages entries must be PythonPackagePin, not {type(pin).__name__}")
717
+ if not isinstance(pin.name, str) or not pin.name:
718
+ raise ValueError(f"pythonPackages: each entry needs a package name, got {pin.name!r}")
719
+ if pin.version is not None and (not isinstance(pin.version, str) or not pin.version):
720
+ raise ValueError(f"pythonPackages: the version of {pin.name!r} must be text, "
721
+ f"got {pin.version!r}")
722
+ normalized = normalize_python_package_name(pin.name)
723
+ if normalized in seen:
724
+ raise ValueError(f"pythonPackages: package {pin.name!r} is listed twice")
725
+ seen.add(normalized)
726
+ return True
727
+
728
+ @staticmethod
729
+ def _read_python_packages(value) -> list:
730
+ if value is None or value == "":
731
+ return []
732
+ if isinstance(value, str):
733
+ value = json.loads(value)
734
+ if not isinstance(value, list) or not all(isinstance(v, dict) for v in value):
735
+ raise ValueError('pythonPackages must be a list of {"name": ..., "version": ...}')
736
+ return [PythonPackagePin.from_dict(v) for v in value]
737
+
666
738
  def check_path(self) -> bool:
667
739
  """
668
740
  validate that steps create a valid path.
669
741
  return true if path is valid, otherwise raise errors
670
742
  """
743
+ self.check_python_packages()
671
744
 
672
745
  followed = [] # all steps already followed
673
746
  produced = [] # all outputs already produced
@@ -1022,6 +1095,8 @@ class Pipeline(ApiDto):
1022
1095
  self.plots = json.loads(obj["plots"])
1023
1096
  else:
1024
1097
  self.plots = obj["plots"]
1098
+ if "pythonPackages" in obj.keys():
1099
+ self.python_packages = self._read_python_packages(obj["pythonPackages"])
1025
1100
  if "createdById" in obj.keys() and obj["createdById"] is not None:
1026
1101
  self.createdById = obj["createdById"]
1027
1102
  if "createdDate" in obj.keys() and obj["createdDate"] is not None:
@@ -1060,6 +1135,15 @@ class Pipeline(ApiDto):
1060
1135
  if self.variables is not None:
1061
1136
  self.check_variables()
1062
1137
  obj["variables"] = dict(self.variables) if as_git else json.dumps(self.variables)
1138
+ # DEV-6120. In git only when not empty, so pipeline files that import nothing do not
1139
+ # change. Elsewhere always, as text like steps / plots / variables (the C# column):
1140
+ # an update that leaves the key out keeps the stored list, so "[]" is how it clears.
1141
+ pins = [pin.to_dict() for pin in self.python_packages]
1142
+ if as_git:
1143
+ if pins:
1144
+ obj["pythonPackages"] = pins
1145
+ else:
1146
+ obj["pythonPackages"] = json.dumps(pins)
1063
1147
  if self.template_id is not None:
1064
1148
  obj["templateId"] = str(self.template_id)
1065
1149
  if self.experiment_id is not None:
@@ -1,3 +1,4 @@
1
+ import contextlib
1
2
  import json
2
3
  import os
3
4
  import wizata_dsapi
@@ -39,6 +40,10 @@ class PipelineImage(ApiDto):
39
40
  self.datapoints = {}
40
41
  self.scripts = []
41
42
  self.models = {}
43
+ # DEV-6120: the Python packages the pipeline pins, bundled at build time
44
+ # (python_packages.json + python_packages/<sha256>.whl), mounted by the loader.
45
+ self.python_package_index = []
46
+ self.python_package_files = {}
42
47
 
43
48
  @classmethod
44
49
  def route(cls):
@@ -102,26 +107,32 @@ class PipelineImage(ApiDto):
102
107
  @classmethod
103
108
  def load(cls,
104
109
  pipeline_image_id: str = None,
105
- g_stream=None) -> 'PipelineImage':
110
+ g_stream=None,
111
+ mount=None) -> 'PipelineImage':
106
112
  """
107
113
  deserialize from a stream (e.g. file handle).
108
114
  :param pipeline_image_id: id of the image.
109
115
  :param g_stream: readable stream.
116
+ :param mount: see :meth:`loads`.
110
117
  :return: PipelineImage
111
118
  """
112
119
  if g_stream is None:
113
120
  raise ValueError("No stream provided")
114
121
  g_bytes = g_stream.read()
115
- return cls.loads(pipeline_image_id=pipeline_image_id, g_bytes=g_bytes)
122
+ return cls.loads(pipeline_image_id=pipeline_image_id, g_bytes=g_bytes, mount=mount)
116
123
 
117
124
  @classmethod
118
125
  def loads(cls,
119
126
  pipeline_image_id: str = None,
120
- g_bytes=None) -> 'PipelineImage':
127
+ g_bytes=None,
128
+ mount=None) -> 'PipelineImage':
121
129
  """
122
130
  deserialize from a bytes array an image.
123
131
  :param pipeline_image_id: id of the image.
124
132
  :param g_bytes: from loaded content as bytes.
133
+ :param mount: called with the image once its pipeline and Python packages are read;
134
+ returns the context manager the models are unpickled in (DEV-6120: the packages
135
+ they were pickled against made importable). None: no packages are mounted.
125
136
  :return: PipelineImage
126
137
  """
127
138
  # unpack the content
@@ -142,6 +153,13 @@ class PipelineImage(ApiDto):
142
153
  pipeline._load_related_entities(pipeline_dict)
143
154
  image.pipeline = pipeline
144
155
 
156
+ # DEV-6120: the Python packages the pipeline pins, as bundled at build time
157
+ if "python_packages.json" in g_files:
158
+ image.python_package_index = json.loads(g_files["python_packages.json"])
159
+ image.python_package_files = {
160
+ k[len("python_packages/"):].rsplit(".", 1)[0]: v for k, v in g_files.items()
161
+ if k.startswith("python_packages/") and k.endswith((".whl", ".zip"))}
162
+
145
163
  # load datapoints if present
146
164
  if "datapoints.json" in g_files:
147
165
  datapoints_json = json.loads(g_files['datapoints.json'])
@@ -177,34 +195,39 @@ class PipelineImage(ApiDto):
177
195
  if json_file not in json_files:
178
196
  raise KeyError(f"missing json file for script pkl {pkl_file}")
179
197
 
180
- # process the models
181
- model_files = {k: v for k, v in g_files.items() if k.startswith('models/')}
182
- for pkl_file in model_files:
183
- parts = pkl_file.split('/')
184
- if len(parts) == 2:
185
- identifier = os.path.splitext(parts[1])[0]
186
- else:
187
- identifier = parts[1]
188
-
189
- if identifier not in image.models:
190
- key, twin_hardware_id, property_value, alias = wizata_dsapi.ModelInfo.split_identifier(identifier)
191
- image.models[identifier] = wizata_dsapi.ModelInfo(
192
- key=key,
193
- twin_hardware_id=twin_hardware_id,
194
- property_value=property_value,
195
- alias=alias
196
- )
197
- if len(parts) == 2:
198
- image.models[identifier].file_format = os.path.splitext(parts[1])[1].lstrip('.')
199
- image.models[identifier].uncompress_bytes_to_model(g_files[pkl_file])
200
- if len(parts) == 3:
201
- image.models[identifier].add_file(wizata_dsapi.ModelFile(
202
- full_path=pkl_file,
203
- path=parts[2],
204
- content=g_files[pkl_file]
205
- ))
198
+ # process the models, in the packages they were pickled against (DEV-6120)
199
+ with (mount(image) if mount is not None else contextlib.nullcontext()):
200
+ cls._load_models(image, g_files)
206
201
 
207
202
  return image
208
203
 
209
204
  except Exception as e:
210
205
  raise Exception(f'invalid image - {e}')
206
+
207
+ @staticmethod
208
+ def _load_models(image: 'PipelineImage', g_files: dict) -> None:
209
+ model_files = {k: v for k, v in g_files.items() if k.startswith('models/')}
210
+ for pkl_file in model_files:
211
+ parts = pkl_file.split('/')
212
+ if len(parts) == 2:
213
+ identifier = os.path.splitext(parts[1])[0]
214
+ else:
215
+ identifier = parts[1]
216
+
217
+ if identifier not in image.models:
218
+ key, twin_hardware_id, property_value, alias = wizata_dsapi.ModelInfo.split_identifier(identifier)
219
+ image.models[identifier] = wizata_dsapi.ModelInfo(
220
+ key=key,
221
+ twin_hardware_id=twin_hardware_id,
222
+ property_value=property_value,
223
+ alias=alias
224
+ )
225
+ if len(parts) == 2:
226
+ image.models[identifier].file_format = os.path.splitext(parts[1])[1].lstrip('.')
227
+ image.models[identifier].uncompress_bytes_to_model(g_files[pkl_file])
228
+ if len(parts) == 3:
229
+ image.models[identifier].add_file(wizata_dsapi.ModelFile(
230
+ full_path=pkl_file,
231
+ path=parts[2],
232
+ content=g_files[pkl_file]
233
+ ))
@@ -0,0 +1,222 @@
1
+ """
2
+ Python packages a pipeline imports (DEV-6120, epic DEV-6119).
3
+
4
+ A customer's own code, typically scikit-learn classes in an installable package, made
5
+ importable where a pipeline runs (runners, pipeline images, edges) under its original
6
+ dotted name, so models pickled against it load unchanged.
7
+
8
+ PythonPackage — one package, named as pip names it: the name is normalised the pip way
9
+ (``XpertRef_Tools`` is ``xpertref-tools``). ``source`` says where its code comes from:
10
+
11
+ - ``upload``: pure-Python wheels uploaded one version at a time (option A);
12
+ - ``git``: the ``packages/<name>/`` folder of the attached repository (option B), whose
13
+ version is the commit a pipeline is read at.
14
+
15
+ ``import_names`` are the top-level modules it provides (``xpertref``). ``versions`` is
16
+ filled on read only.
17
+
18
+ PythonPackageVersion — one uploaded wheel of an ``upload`` package. Immutable: a version is
19
+ never replaced, a fix is a new version.
20
+
21
+ The terms are pip's and PyPI's (name, version). "Release" is not used: Wizata already has
22
+ release branches and edge releases. This replaces the script "library" concept
23
+ (``ScriptLibrary``, ``ScriptType.MODULE``), which was never used and is removed.
24
+ """
25
+ import re
26
+ from datetime import datetime, timezone
27
+ from enum import Enum
28
+ from typing import List, Optional
29
+
30
+
31
+ def normalize_python_package_name(name: str) -> str:
32
+ """
33
+ A package name as pip compares it (PEP 503): ``XpertRef_Tools.v2`` -> ``xpertref-tools-v2``.
34
+ Validating it is the package client's job.
35
+ """
36
+ return re.sub(r"[-_.]+", "-", name).lower()
37
+
38
+
39
+ class PythonPackageSource(Enum):
40
+ """Where a Python package's code comes from."""
41
+ UPLOAD = "upload"
42
+ GIT = "git"
43
+
44
+
45
+ def _ts_out(value):
46
+ if value is None:
47
+ return None
48
+ if value.tzinfo is None:
49
+ value = value.replace(tzinfo=timezone.utc)
50
+ return value.isoformat()
51
+
52
+
53
+ def _ts_in(value):
54
+ if value is None or isinstance(value, datetime):
55
+ return value
56
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
57
+
58
+
59
+ def _first(data: dict, *keys):
60
+ for key in keys:
61
+ if data.get(key) is not None:
62
+ return data[key]
63
+ return None
64
+
65
+
66
+ class PythonPackageVersion:
67
+ """
68
+ One uploaded wheel, stored in PostgreSQL ``"PythonPackageVersion"``. PK (``name``,
69
+ ``version``).
70
+
71
+ :ivar str name: the package's normalised name.
72
+ :ivar str version: the version, normalised as pip does (PEP 440).
73
+ :ivar str filename: the wheel's file name.
74
+ :ivar str sha256: hex digest of the wheel.
75
+ :ivar int size: the wheel's size in bytes.
76
+ :ivar list requires_dist: the wheel's ``Requires-Dist`` lines.
77
+ :ivar str requires_python: the wheel's ``Requires-Python``.
78
+ :ivar str uploaded_by: the user who uploaded it.
79
+ :ivar datetime uploaded_at: when it was uploaded.
80
+ """
81
+
82
+ def __init__(self, name: str = None, version: str = None, filename: str = None,
83
+ sha256: str = None, size: int = None, requires_dist: list = None,
84
+ requires_python: str = None):
85
+ self.name: Optional[str] = name
86
+ self.version: Optional[str] = version
87
+ self.filename: Optional[str] = filename
88
+ self.sha256: Optional[str] = sha256
89
+ self.size: Optional[int] = size
90
+ self.requires_dist: Optional[List[str]] = requires_dist
91
+ self.requires_python: Optional[str] = requires_python
92
+ self.uploaded_by: Optional[str] = None
93
+ self.uploaded_at: Optional[datetime] = None
94
+
95
+ def api_id(self) -> str:
96
+ return f"{self.name}=={self.version}"
97
+
98
+ def to_dict(self) -> dict:
99
+ obj = {}
100
+ for key, value in (("name", self.name), ("version", self.version),
101
+ ("filename", self.filename), ("sha256", self.sha256),
102
+ ("size", self.size), ("requiresDist", self.requires_dist),
103
+ ("requiresPython", self.requires_python),
104
+ ("uploadedBy", self.uploaded_by),
105
+ ("uploadedAt", _ts_out(self.uploaded_at))):
106
+ if value is not None:
107
+ obj[key] = value
108
+ return obj
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict) -> "PythonPackageVersion":
112
+ obj = cls(name=data.get("name"), version=data.get("version"),
113
+ filename=data.get("filename"), sha256=data.get("sha256"),
114
+ size=data.get("size"),
115
+ requires_dist=_first(data, "requiresDist", "requires_dist"),
116
+ requires_python=_first(data, "requiresPython", "requires_python"))
117
+ obj.uploaded_by = _first(data, "uploadedBy", "uploaded_by")
118
+ obj.uploaded_at = _ts_in(_first(data, "uploadedAt", "uploaded_at"))
119
+ return obj
120
+
121
+ def __repr__(self) -> str:
122
+ return f"PythonPackageVersion({self.name}=={self.version})"
123
+
124
+
125
+ class PythonPackage:
126
+ """
127
+ A Python package, stored in PostgreSQL ``"PythonPackage"``. PK ``name``.
128
+
129
+ :ivar str name: the name, normalised as pip does.
130
+ :ivar PythonPackageSource source: upload or git; never changes.
131
+ :ivar list import_names: the top-level modules it provides.
132
+ :ivar str description: free text.
133
+ :ivar str created_by: the user who created it.
134
+ :ivar datetime created_at: when it was created.
135
+ :ivar datetime updated_at: when it last changed.
136
+ :ivar str problem: why it cannot be used, when it cannot (a git folder that does not
137
+ compile, a name clash with the repository); set by the platform.
138
+ :ivar list versions: its uploaded versions, newest first (filled on read only).
139
+ """
140
+
141
+ @classmethod
142
+ def route(cls):
143
+ return "pythonpackages"
144
+
145
+ def __init__(self, name: str = None, source: PythonPackageSource = None,
146
+ import_names: list = None, description: str = None):
147
+ self.name: Optional[str] = name
148
+ self.source: Optional[PythonPackageSource] = source
149
+ self.import_names: List[str] = import_names if import_names is not None else []
150
+ self.description: Optional[str] = description
151
+ self.created_by: Optional[str] = None
152
+ self.created_at: Optional[datetime] = None
153
+ self.updated_at: Optional[datetime] = None
154
+ self.problem: Optional[str] = None
155
+ self.versions: Optional[List[PythonPackageVersion]] = None
156
+
157
+ def api_id(self) -> str:
158
+ return self.name
159
+
160
+ def to_dict(self) -> dict:
161
+ obj = {}
162
+ if self.name is not None:
163
+ obj["name"] = self.name
164
+ if self.source is not None:
165
+ obj["source"] = self.source.value
166
+ obj["importNames"] = list(self.import_names) if isinstance(self.import_names, list) \
167
+ else self.import_names
168
+ for key, value in (("description", self.description), ("createdBy", self.created_by),
169
+ ("createdAt", _ts_out(self.created_at)),
170
+ ("updatedAt", _ts_out(self.updated_at)), ("problem", self.problem)):
171
+ if value is not None:
172
+ obj[key] = value
173
+ if self.versions is not None:
174
+ obj["versions"] = [v.to_dict() for v in self.versions]
175
+ return obj
176
+
177
+ @classmethod
178
+ def from_dict(cls, data: dict) -> "PythonPackage":
179
+ source = data.get("source")
180
+ obj = cls(name=data.get("name"),
181
+ source=PythonPackageSource(source) if source is not None else None,
182
+ import_names=_first(data, "importNames", "import_names"),
183
+ description=data.get("description"))
184
+ obj.created_by = _first(data, "createdBy", "created_by")
185
+ obj.created_at = _ts_in(_first(data, "createdAt", "created_at"))
186
+ obj.updated_at = _ts_in(_first(data, "updatedAt", "updated_at"))
187
+ obj.problem = data.get("problem")
188
+ if data.get("versions") is not None:
189
+ obj.versions = [PythonPackageVersion.from_dict(v) for v in data["versions"]]
190
+ return obj
191
+
192
+ def __repr__(self) -> str:
193
+ source = self.source.value if self.source is not None else None
194
+ return f"PythonPackage(name={self.name!r}, source={source!r})"
195
+
196
+
197
+ class PythonPackagePin:
198
+ """
199
+ One entry of a pipeline's ``pythonPackages`` list: a package the pipeline imports.
200
+
201
+ :ivar str name: the package's name.
202
+ :ivar str version: the uploaded version it runs with; None for a git package, whose
203
+ code is read at the commit the pipeline is read at.
204
+ """
205
+
206
+ def __init__(self, name: str = None, version: str = None):
207
+ self.name: Optional[str] = name
208
+ self.version: Optional[str] = version
209
+
210
+ def to_dict(self) -> dict:
211
+ obj = {"name": self.name}
212
+ if self.version is not None:
213
+ obj["version"] = self.version
214
+ return obj
215
+
216
+ @classmethod
217
+ def from_dict(cls, data: dict) -> "PythonPackagePin":
218
+ return cls(name=data.get("name"), version=data.get("version"))
219
+
220
+ def __repr__(self) -> str:
221
+ return f"PythonPackagePin({self.name}=={self.version})" if self.version \
222
+ else f"PythonPackagePin({self.name})"
@@ -16,12 +16,13 @@ class ScriptType(Enum):
16
16
  defines the type of script execution.
17
17
  - "wizata" builtin wizata library function (wizata. prefix)
18
18
  - "api" dill-serialized script stored on backend (DB + blob .pkl)
19
- - "module" function from an installed custom python module
20
19
  - "raw" raw .py source stored on blob, compiled at runtime
20
+
21
+ A customer's own package is not a script type: a pipeline imports it as a Python
22
+ package (``PythonPackage``, DEV-6120).
21
23
  """
22
24
  WIZATA = 'wizata'
23
25
  API = 'api'
24
- MODULE = 'module'
25
26
  RAW = 'raw'
26
27
 
27
28
 
@@ -94,12 +95,10 @@ class ScriptConfig(ApiDto):
94
95
 
95
96
  def __init__(self,
96
97
  function=None,
97
- library=None,
98
98
  properties_mapping=None,
99
99
  properties: dict = None,
100
100
  script_type=None):
101
101
  self.function = function
102
- self.library = library
103
102
  self.properties = properties
104
103
  self.properties_mapping = properties_mapping
105
104
  self.script_type = script_type
@@ -111,9 +110,7 @@ class ScriptConfig(ApiDto):
111
110
  else:
112
111
  if "function" in obj:
113
112
  self.function = obj["function"]
114
- if "library" in obj:
115
- self.library = obj["library"]
116
- elif "script" in obj:
113
+ if "script" in obj:
117
114
  self.function = obj["script"]
118
115
  if "properties" in obj:
119
116
  self.properties = obj["properties"]
@@ -126,8 +123,6 @@ class ScriptConfig(ApiDto):
126
123
  obj = {
127
124
  "function": self.function
128
125
  }
129
- if self.library is not None:
130
- obj["library"] = self.library
131
126
  if self.properties is not None and isinstance(self.properties, dict):
132
127
  obj["properties"] = self.properties
133
128
  if self.properties_mapping is not None and isinstance(self.properties_mapping, dict):
@@ -139,8 +134,7 @@ class ScriptConfig(ApiDto):
139
134
  def infer_script_type(self):
140
135
  """
141
136
  Return explicit script_type if set, otherwise infer from convention:
142
- - function starts with "wizata." -> LIBRARY_WIZATA
143
- - library field is set -> LIBRARY_CUSTOM
137
+ - function starts with "wizata." -> WIZATA (built-in)
144
138
  - else -> API
145
139
  Note: RAW cannot be inferred; must be set explicitly.
146
140
  """
@@ -148,8 +142,6 @@ class ScriptConfig(ApiDto):
148
142
  return self.script_type
149
143
  if self.function is not None and str(self.function).startswith("wizata."):
150
144
  return ScriptType.WIZATA
151
- if self.library is not None:
152
- return ScriptType.MODULE
153
145
  return ScriptType.API
154
146
 
155
147
 
@@ -160,10 +152,8 @@ class Script(ApiDto):
160
152
  :ivar uuid.UUID script_id: technical id of the script.
161
153
  :ivar str name: unique name of the script function (unique key within its type).
162
154
  :ivar str description: human-readable description of what the script does.
163
- :ivar ScriptType script_type: type of script execution (api, raw, module, wizata).
155
+ :ivar ScriptType script_type: type of script execution (api, raw, wizata).
164
156
  :ivar str version: version string of the script.
165
- :ivar str library: key of the ScriptLibrary this script belongs to (module type only).
166
- :ivar str module: Python import path for import_module() (module type only, e.g. 'my_lib.transforms').
167
157
  :ivar callable function: the function as a callable object (not serialized).
168
158
  :ivar str source: python source code of the script (not serialized).
169
159
  """
@@ -187,7 +177,7 @@ class Script(ApiDto):
187
177
  return "json"
188
178
 
189
179
  def __init__(self, script_id=None, description=None, function=None,
190
- script_type=None, version=None, library=None, category=None,
180
+ script_type=None, version=None, category=None,
191
181
  properties=None):
192
182
 
193
183
  # Id
@@ -200,8 +190,6 @@ class Script(ApiDto):
200
190
  self.description = description
201
191
  self.script_type = script_type
202
192
  self.version = version
203
- self.library = library
204
- self.module = None
205
193
  self.category = category
206
194
  self.properties = properties if properties is not None else {}
207
195
 
@@ -265,10 +253,6 @@ class Script(ApiDto):
265
253
  obj["type"] = self.script_type.value
266
254
  if self.version is not None:
267
255
  obj["version"] = self.version
268
- if self.library is not None:
269
- obj["library"] = self.library
270
- if self.module is not None:
271
- obj["module"] = self.module
272
256
  if self.source is not None:
273
257
  obj["source"] = self.source
274
258
  if self.category is not None:
@@ -300,10 +284,6 @@ class Script(ApiDto):
300
284
  self.script_type = ScriptType(obj["type"])
301
285
  if "version" in obj:
302
286
  self.version = obj["version"]
303
- if "library" in obj:
304
- self.library = obj["library"]
305
- if "module" in obj:
306
- self.module = obj["module"]
307
287
  if "source" in obj and obj["source"] is not None:
308
288
  self.source = obj["source"]
309
289
  if "category" in obj and obj["category"] is not None:
@@ -404,124 +384,6 @@ class Script(ApiDto):
404
384
  self.source = source_code
405
385
 
406
386
 
407
- class ScriptLibrary:
408
- """
409
- Represents a custom library registration.
410
- Describes an installed Python package that exposes functions usable in pipelines.
411
-
412
- Metadata is stored in PostgreSQL; function entries are Script rows with
413
- script_type=MODULE and a foreign key back to this library's key.
414
-
415
- :ivar str key: unique identifier/key for this library registration.
416
- :ivar str package_name: pip package name (e.g. 'my-custom-lib').
417
- :ivar str module_name: Python import module name (e.g. 'my_custom_lib').
418
- :ivar str version: version string of the registered library.
419
- :ivar str description: human-readable description of the library.
420
- :ivar list functions: list of Script entries (script_type=MODULE).
421
- """
422
-
423
- def __init__(self,
424
- key: str = None,
425
- package_name: str = None,
426
- module_name: str = None,
427
- version: str = None,
428
- description: str = None,
429
- functions: list = None):
430
- self.key = key
431
- self.package_name = package_name
432
- self.module_name = module_name
433
- self.version = version
434
- self.description = description
435
- if functions is None:
436
- functions = []
437
- self.functions = functions
438
-
439
- @classmethod
440
- def from_dict(cls, data: dict):
441
- obj = ScriptLibrary()
442
- obj.key = data.get("key")
443
- obj.package_name = data.get("packageName") or data.get("package_name")
444
- obj.module_name = data.get("moduleName") or data.get("module_name")
445
- obj.version = data.get("version")
446
- obj.description = data.get("description")
447
- if "functions" in data and data["functions"] is not None:
448
- for func_data in data["functions"]:
449
- script = Script.from_dict(func_data)
450
- script.script_type = ScriptType.MODULE
451
- script.library = obj.key
452
- # "module" field = import path; falls back to library module_name
453
- if script.module is None:
454
- script.module = obj.module_name
455
- obj.functions.append(script)
456
- return obj
457
-
458
- def to_dict(self) -> dict:
459
- obj = {}
460
- if self.key is not None:
461
- obj["key"] = self.key
462
- if self.package_name is not None:
463
- obj["packageName"] = self.package_name
464
- if self.module_name is not None:
465
- obj["moduleName"] = self.module_name
466
- if self.version is not None:
467
- obj["version"] = self.version
468
- if self.description is not None:
469
- obj["description"] = self.description
470
- if self.functions is not None and len(self.functions) > 0:
471
- obj["functions"] = [f.to_dict() for f in self.functions]
472
- return obj
473
-
474
- def get_function(self, name: str):
475
- """
476
- Find a function entry by name.
477
- :param name: function name to search.
478
- :return: Script or None.
479
- """
480
- for func in self.functions:
481
- if func.name == name:
482
- return func
483
- return None
484
-
485
-
486
- def discover_from_wheel(wheel_source) -> ScriptLibrary:
487
- """
488
- Extract wizata.json manifest from a .whl file and parse into ScriptLibrary.
489
-
490
- The manifest should be placed inside the top-level package directory
491
- of the wheel (e.g. ``my_custom_lib/wizata.json``).
492
-
493
- :param wheel_source: filesystem path (str) or bytes content of the .whl file.
494
- :return: ScriptLibrary parsed from the manifest.
495
- :raises FileNotFoundError: if no wizata.json found in the wheel.
496
- :raises ValueError: if wizata.json is missing the required 'key' field.
497
- """
498
- import zipfile
499
- import json
500
- import io
501
-
502
- if isinstance(wheel_source, bytes):
503
- file_obj = io.BytesIO(wheel_source)
504
- else:
505
- file_obj = wheel_source
506
-
507
- with zipfile.ZipFile(file_obj, 'r') as zf:
508
- candidates = [n for n in zf.namelist()
509
- if n.endswith('/wizata.json') or n == 'wizata.json']
510
- if not candidates:
511
- raise FileNotFoundError(
512
- f"no wizata.json found in wheel"
513
- )
514
- # prefer the shortest path (top-level package)
515
- candidates.sort(key=len)
516
- raw = zf.read(candidates[0])
517
-
518
- data = json.loads(raw)
519
- if "key" not in data:
520
- raise ValueError("wizata.json must contain a 'key' field")
521
-
522
- return ScriptLibrary.from_dict(data)
523
-
524
-
525
387
  class Function:
526
388
  """
527
389
  Python Code Function
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev25"
@@ -30,6 +30,7 @@ from .plot import Plot
30
30
  from .request import Request
31
31
  from .formula import normalize_formulas
32
32
  from .mlmodel import ModelInfo, ModelList, ModelFile
33
+ from .python_package import PythonPackage, PythonPackageVersion, PythonPackagePin
33
34
  from .experiment import Experiment
34
35
  from .script import Script
35
36
  from .execution import Execution, ExecutionStatus
@@ -661,7 +662,7 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
661
662
  Download a script with its artifacts (function/source) as a dill binary.
662
663
 
663
664
  :param name: script name.
664
- :param script_type: one of 'api', 'raw', 'module' (default: 'api').
665
+ :param script_type: one of 'api', 'raw' (default: 'api').
665
666
  :return: Script with function loaded.
666
667
  """
667
668
  params = {'type': script_type}
@@ -1375,7 +1376,8 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1375
1376
 
1376
1377
  def upload_model(self,
1377
1378
  model_info: ModelInfo,
1378
- bytes_content = None):
1379
+ bytes_content = None,
1380
+ python_packages: list = None):
1379
1381
  """
1380
1382
  upload a model within the model repository.
1381
1383
  - by default use model_info.trained_model and convert it to a pickle
@@ -1383,9 +1385,22 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1383
1385
  - model_info.file_format must be set properly to 'pkl' or 'pt'
1384
1386
  :param model_info: model info, with at least key (+twin, +property, +alias) and trained_model.
1385
1387
  :param bytes_content: bytes[] of your torch or pickle model.
1388
+ :param python_packages: the Python packages (uploaded with upload_python_package) the
1389
+ model was trained with, each with its version: PythonPackagePin or
1390
+ {"name": ..., "version": ...}. A deploy then warns when the pipeline pins
1391
+ another version (DEV-6120).
1386
1392
  """
1387
1393
  if model_info.trained_model is None and bytes_content is None:
1388
1394
  raise ValueError("model_info must have a trained model (to pickle) or bytes content")
1395
+ if python_packages is not None:
1396
+ declared = []
1397
+ for pin in python_packages:
1398
+ entry = pin.to_dict() if isinstance(pin, PythonPackagePin) else dict(pin)
1399
+ if not entry.get("name") or not entry.get("version"):
1400
+ raise ValueError(f"python_packages: each needs a name and the version the model "
1401
+ f"was trained with, got {entry!r}")
1402
+ declared.append({"name": entry["name"], "version": str(entry["version"])})
1403
+ model_info.metadata = {**(model_info.metadata or {}), "pythonPackages": declared}
1389
1404
  if bytes_content is None:
1390
1405
  bytes_content = pickle.dumps(model_info.trained_model)
1391
1406
  files = {
@@ -1429,6 +1444,92 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1429
1444
  else:
1430
1445
  raise self.__raise_error(response)
1431
1446
 
1447
+ # ------------------------------------------------------------------ Python packages (DEV-6121)
1448
+
1449
+ @staticmethod
1450
+ def _quote(value) -> str:
1451
+ return urllib.parse.quote(str(value), safe="")
1452
+
1453
+ def upload_python_package(self, wheel, filename: str = None,
1454
+ description: str = None) -> PythonPackageVersion:
1455
+ """
1456
+ Upload a version of a Python package your pipelines import: the pure-Python wheel
1457
+ ``python -m build --wheel`` makes (e.g. ``dist/xpertref-1.4.0-py3-none-any.whl``).
1458
+ Its name, version and requirements come from the wheel; the package is created by
1459
+ its first upload. A version is immutable: a fix is a new version.
1460
+
1461
+ :param wheel: path to the .whl file, or its bytes (then give ``filename``).
1462
+ :param filename: the wheel's file name, when ``wheel`` is bytes.
1463
+ :param description: the package's description, on its first upload.
1464
+ :return: the uploaded PythonPackageVersion.
1465
+ """
1466
+ if isinstance(wheel, (bytes, bytearray)):
1467
+ if not filename:
1468
+ raise ValueError("give the wheel's file name with its bytes "
1469
+ "(e.g. xpertref-1.4.0-py3-none-any.whl)")
1470
+ data = bytes(wheel)
1471
+ else:
1472
+ path = str(wheel)
1473
+ filename = filename or os.path.basename(path)
1474
+ with open(path, "rb") as f:
1475
+ data = f.read()
1476
+ headers = self.__header()
1477
+ headers.pop("Content-Type", None)
1478
+ response = requests.post(self.__url() + "pythonpackages/",
1479
+ headers=headers,
1480
+ data={"description": description} if description else None,
1481
+ files={"file": (filename, data, "application/octet-stream")})
1482
+ if response.status_code != 201:
1483
+ raise self.__raise_error(response)
1484
+ return PythonPackageVersion.from_dict(response.json())
1485
+
1486
+ def search_python_packages(self, search_query=None) -> PagedQueryResult:
1487
+ """
1488
+ :param search_query: SearchQuery or dict; filters on ``name`` and ``source``
1489
+ (upload / git), ``_search`` over name and description.
1490
+ :return: PagedQueryResult with PythonPackage results.
1491
+ """
1492
+ if search_query is None:
1493
+ search_query = SearchQuery()
1494
+ elif isinstance(search_query, dict):
1495
+ search_query = SearchQuery.from_dict(search_query)
1496
+ response = self._planner_request("POST", "pythonpackages/search", body=search_query.to_dict())
1497
+ return PagedQueryResult.from_dict(response.json(), dto_class=PythonPackage)
1498
+
1499
+ def get_python_package(self, name: str):
1500
+ """
1501
+ :return: the PythonPackage with its versions (newest first), or None.
1502
+ """
1503
+ response = self._planner_request("GET", f"pythonpackages/{self._quote(name)}/",
1504
+ expected=(200, 404))
1505
+ return PythonPackage.from_dict(response.json()) if response.status_code == 200 else None
1506
+
1507
+ def download_python_package(self, name: str, version: str, path: str = None):
1508
+ """
1509
+ :param path: where to write the wheel; None returns its bytes.
1510
+ :return: the wheel's bytes, or ``path``.
1511
+ """
1512
+ response = requests.get(
1513
+ self.__url() + f"pythonpackages/{self._quote(name)}/versions/{self._quote(version)}/download",
1514
+ headers=self.__header())
1515
+ if response.status_code != 200:
1516
+ raise self.__raise_error(response)
1517
+ if path is None:
1518
+ return response.content
1519
+ with open(path, "wb") as f:
1520
+ f.write(response.content)
1521
+ return path
1522
+
1523
+ def delete_python_package_version(self, name: str, version: str):
1524
+ """Refused (409) while a stored pipeline pins it."""
1525
+ self._planner_request("DELETE",
1526
+ f"pythonpackages/{self._quote(name)}/versions/{self._quote(version)}/",
1527
+ expected=(204,))
1528
+
1529
+ def delete_python_package(self, name: str):
1530
+ """Refused (409) while it has versions, or a stored pipeline imports it."""
1531
+ self._planner_request("DELETE", f"pythonpackages/{self._quote(name)}/", expected=(204,))
1532
+
1432
1533
  def upsert_experiment(self, key: str, name: str, pipeline=None, twin=None):
1433
1534
  """
1434
1535
  Upsert an experiment.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev24
3
+ Version: 2.2.0.dev25
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -42,6 +42,7 @@ wizata_dsapi/pipeline.py
42
42
  wizata_dsapi/pipeline_image.py
43
43
  wizata_dsapi/plot.py
44
44
  wizata_dsapi/product.py
45
+ wizata_dsapi/python_package.py
45
46
  wizata_dsapi/request.py
46
47
  wizata_dsapi/script.py
47
48
  wizata_dsapi/search.py
@@ -1 +0,0 @@
1
- __version__ = "2.2.0.dev24"