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.
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/__init__.py +3 -1
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/context.py +3 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/pipeline.py +85 -1
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/pipeline_image.py +52 -29
- wizata_dsapi-2.2.0.dev25/wizata_dsapi/python_package.py +222 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/script.py +7 -145
- wizata_dsapi-2.2.0.dev25/wizata_dsapi/version.py +1 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizata_dsapi_client.py +103 -2
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/SOURCES.txt +1 -0
- wizata_dsapi-2.2.0.dev24/wizata_dsapi/version.py +0 -1
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/README.rst +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/setup.cfg +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/setup.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_config.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_dto.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/api_interface.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/bucket.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/business_label.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_discovery.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_policy.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/connection_types.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dashboard.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dataframe_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/datapoint.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/datastore.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/deployment.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/ds_dataframe.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/dsapi_json_encoder.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_config.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_device.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_module.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/edge_package.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/evaluation.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/execution.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/execution_log.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/experiment.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/formula.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/graylog_log.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/group_system.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/ilogger.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/insight.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/inventory.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/mlmodel.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/mobile_asset.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/model_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/models/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/models/common.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/notification.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/paged_query_result.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plot.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/common.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/plots/theme.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/product.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/request.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/scripts/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/scripts/common.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/search.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/solution_component.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/streamlit_utils.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/template.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/template_config.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/trigger.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/twin.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/twinregistration.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/user.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizard_function.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/wizard_request.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi/words.py +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/requires.txt +0 -0
- {wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/top_level.txt +0 -0
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
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 "
|
|
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." ->
|
|
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,
|
|
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,
|
|
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'
|
|
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 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.0.dev24"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wizata_dsapi-2.2.0.dev24 → wizata_dsapi-2.2.0.dev25}/wizata_dsapi.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|