dtlpy 1.115.44__py3-none-any.whl → 1.117.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. dtlpy/__init__.py +491 -491
  2. dtlpy/__version__.py +1 -1
  3. dtlpy/assets/__init__.py +26 -26
  4. dtlpy/assets/code_server/config.yaml +2 -2
  5. dtlpy/assets/code_server/installation.sh +24 -24
  6. dtlpy/assets/code_server/launch.json +13 -13
  7. dtlpy/assets/code_server/settings.json +2 -2
  8. dtlpy/assets/main.py +53 -53
  9. dtlpy/assets/main_partial.py +18 -18
  10. dtlpy/assets/mock.json +11 -11
  11. dtlpy/assets/model_adapter.py +83 -83
  12. dtlpy/assets/package.json +61 -61
  13. dtlpy/assets/package_catalog.json +29 -29
  14. dtlpy/assets/package_gitignore +307 -307
  15. dtlpy/assets/service_runners/__init__.py +33 -33
  16. dtlpy/assets/service_runners/converter.py +96 -96
  17. dtlpy/assets/service_runners/multi_method.py +49 -49
  18. dtlpy/assets/service_runners/multi_method_annotation.py +54 -54
  19. dtlpy/assets/service_runners/multi_method_dataset.py +55 -55
  20. dtlpy/assets/service_runners/multi_method_item.py +52 -52
  21. dtlpy/assets/service_runners/multi_method_json.py +52 -52
  22. dtlpy/assets/service_runners/single_method.py +37 -37
  23. dtlpy/assets/service_runners/single_method_annotation.py +43 -43
  24. dtlpy/assets/service_runners/single_method_dataset.py +43 -43
  25. dtlpy/assets/service_runners/single_method_item.py +41 -41
  26. dtlpy/assets/service_runners/single_method_json.py +42 -42
  27. dtlpy/assets/service_runners/single_method_multi_input.py +45 -45
  28. dtlpy/assets/voc_annotation_template.xml +23 -23
  29. dtlpy/caches/base_cache.py +32 -32
  30. dtlpy/caches/cache.py +473 -473
  31. dtlpy/caches/dl_cache.py +201 -201
  32. dtlpy/caches/filesystem_cache.py +89 -89
  33. dtlpy/caches/redis_cache.py +84 -84
  34. dtlpy/dlp/__init__.py +20 -20
  35. dtlpy/dlp/cli_utilities.py +367 -367
  36. dtlpy/dlp/command_executor.py +764 -764
  37. dtlpy/dlp/dlp +1 -1
  38. dtlpy/dlp/dlp.bat +1 -1
  39. dtlpy/dlp/dlp.py +128 -128
  40. dtlpy/dlp/parser.py +651 -651
  41. dtlpy/entities/__init__.py +83 -83
  42. dtlpy/entities/analytic.py +347 -347
  43. dtlpy/entities/annotation.py +1879 -1879
  44. dtlpy/entities/annotation_collection.py +699 -699
  45. dtlpy/entities/annotation_definitions/__init__.py +20 -20
  46. dtlpy/entities/annotation_definitions/base_annotation_definition.py +100 -100
  47. dtlpy/entities/annotation_definitions/box.py +195 -195
  48. dtlpy/entities/annotation_definitions/classification.py +67 -67
  49. dtlpy/entities/annotation_definitions/comparison.py +72 -72
  50. dtlpy/entities/annotation_definitions/cube.py +204 -204
  51. dtlpy/entities/annotation_definitions/cube_3d.py +149 -149
  52. dtlpy/entities/annotation_definitions/description.py +32 -32
  53. dtlpy/entities/annotation_definitions/ellipse.py +124 -124
  54. dtlpy/entities/annotation_definitions/free_text.py +62 -62
  55. dtlpy/entities/annotation_definitions/gis.py +69 -69
  56. dtlpy/entities/annotation_definitions/note.py +139 -139
  57. dtlpy/entities/annotation_definitions/point.py +117 -117
  58. dtlpy/entities/annotation_definitions/polygon.py +182 -182
  59. dtlpy/entities/annotation_definitions/polyline.py +111 -111
  60. dtlpy/entities/annotation_definitions/pose.py +92 -92
  61. dtlpy/entities/annotation_definitions/ref_image.py +86 -86
  62. dtlpy/entities/annotation_definitions/segmentation.py +240 -240
  63. dtlpy/entities/annotation_definitions/subtitle.py +34 -34
  64. dtlpy/entities/annotation_definitions/text.py +85 -85
  65. dtlpy/entities/annotation_definitions/undefined_annotation.py +74 -74
  66. dtlpy/entities/app.py +220 -220
  67. dtlpy/entities/app_module.py +107 -107
  68. dtlpy/entities/artifact.py +174 -174
  69. dtlpy/entities/assignment.py +399 -399
  70. dtlpy/entities/base_entity.py +214 -214
  71. dtlpy/entities/bot.py +113 -113
  72. dtlpy/entities/codebase.py +292 -292
  73. dtlpy/entities/collection.py +38 -38
  74. dtlpy/entities/command.py +169 -169
  75. dtlpy/entities/compute.py +449 -449
  76. dtlpy/entities/dataset.py +1299 -1299
  77. dtlpy/entities/directory_tree.py +44 -44
  78. dtlpy/entities/dpk.py +470 -470
  79. dtlpy/entities/driver.py +235 -235
  80. dtlpy/entities/execution.py +397 -397
  81. dtlpy/entities/feature.py +124 -124
  82. dtlpy/entities/feature_set.py +152 -145
  83. dtlpy/entities/filters.py +798 -798
  84. dtlpy/entities/gis_item.py +107 -107
  85. dtlpy/entities/integration.py +184 -184
  86. dtlpy/entities/item.py +975 -959
  87. dtlpy/entities/label.py +123 -123
  88. dtlpy/entities/links.py +85 -85
  89. dtlpy/entities/message.py +175 -175
  90. dtlpy/entities/model.py +684 -684
  91. dtlpy/entities/node.py +1005 -1005
  92. dtlpy/entities/ontology.py +810 -803
  93. dtlpy/entities/organization.py +287 -287
  94. dtlpy/entities/package.py +657 -657
  95. dtlpy/entities/package_defaults.py +5 -5
  96. dtlpy/entities/package_function.py +185 -185
  97. dtlpy/entities/package_module.py +113 -113
  98. dtlpy/entities/package_slot.py +118 -118
  99. dtlpy/entities/paged_entities.py +299 -299
  100. dtlpy/entities/pipeline.py +624 -624
  101. dtlpy/entities/pipeline_execution.py +279 -279
  102. dtlpy/entities/project.py +394 -394
  103. dtlpy/entities/prompt_item.py +505 -505
  104. dtlpy/entities/recipe.py +301 -301
  105. dtlpy/entities/reflect_dict.py +102 -102
  106. dtlpy/entities/resource_execution.py +138 -138
  107. dtlpy/entities/service.py +974 -963
  108. dtlpy/entities/service_driver.py +117 -117
  109. dtlpy/entities/setting.py +294 -294
  110. dtlpy/entities/task.py +495 -495
  111. dtlpy/entities/time_series.py +143 -143
  112. dtlpy/entities/trigger.py +426 -426
  113. dtlpy/entities/user.py +118 -118
  114. dtlpy/entities/webhook.py +124 -124
  115. dtlpy/examples/__init__.py +19 -19
  116. dtlpy/examples/add_labels.py +135 -135
  117. dtlpy/examples/add_metadata_to_item.py +21 -21
  118. dtlpy/examples/annotate_items_using_model.py +65 -65
  119. dtlpy/examples/annotate_video_using_model_and_tracker.py +75 -75
  120. dtlpy/examples/annotations_convert_to_voc.py +9 -9
  121. dtlpy/examples/annotations_convert_to_yolo.py +9 -9
  122. dtlpy/examples/convert_annotation_types.py +51 -51
  123. dtlpy/examples/converter.py +143 -143
  124. dtlpy/examples/copy_annotations.py +22 -22
  125. dtlpy/examples/copy_folder.py +31 -31
  126. dtlpy/examples/create_annotations.py +51 -51
  127. dtlpy/examples/create_video_annotations.py +83 -83
  128. dtlpy/examples/delete_annotations.py +26 -26
  129. dtlpy/examples/filters.py +113 -113
  130. dtlpy/examples/move_item.py +23 -23
  131. dtlpy/examples/play_video_annotation.py +13 -13
  132. dtlpy/examples/show_item_and_mask.py +53 -53
  133. dtlpy/examples/triggers.py +49 -49
  134. dtlpy/examples/upload_batch_of_items.py +20 -20
  135. dtlpy/examples/upload_items_and_custom_format_annotations.py +55 -55
  136. dtlpy/examples/upload_items_with_modalities.py +43 -43
  137. dtlpy/examples/upload_segmentation_annotations_from_mask_image.py +44 -44
  138. dtlpy/examples/upload_yolo_format_annotations.py +70 -70
  139. dtlpy/exceptions.py +125 -125
  140. dtlpy/miscellaneous/__init__.py +20 -20
  141. dtlpy/miscellaneous/dict_differ.py +95 -95
  142. dtlpy/miscellaneous/git_utils.py +217 -217
  143. dtlpy/miscellaneous/json_utils.py +14 -14
  144. dtlpy/miscellaneous/list_print.py +105 -105
  145. dtlpy/miscellaneous/zipping.py +130 -130
  146. dtlpy/ml/__init__.py +20 -20
  147. dtlpy/ml/base_feature_extractor_adapter.py +27 -27
  148. dtlpy/ml/base_model_adapter.py +1287 -1230
  149. dtlpy/ml/metrics.py +461 -461
  150. dtlpy/ml/predictions_utils.py +274 -274
  151. dtlpy/ml/summary_writer.py +57 -57
  152. dtlpy/ml/train_utils.py +60 -60
  153. dtlpy/new_instance.py +252 -252
  154. dtlpy/repositories/__init__.py +56 -56
  155. dtlpy/repositories/analytics.py +85 -85
  156. dtlpy/repositories/annotations.py +916 -916
  157. dtlpy/repositories/apps.py +383 -383
  158. dtlpy/repositories/artifacts.py +452 -452
  159. dtlpy/repositories/assignments.py +599 -599
  160. dtlpy/repositories/bots.py +213 -213
  161. dtlpy/repositories/codebases.py +559 -559
  162. dtlpy/repositories/collections.py +332 -332
  163. dtlpy/repositories/commands.py +152 -152
  164. dtlpy/repositories/compositions.py +61 -61
  165. dtlpy/repositories/computes.py +439 -439
  166. dtlpy/repositories/datasets.py +1585 -1504
  167. dtlpy/repositories/downloader.py +1157 -923
  168. dtlpy/repositories/dpks.py +433 -433
  169. dtlpy/repositories/drivers.py +482 -482
  170. dtlpy/repositories/executions.py +815 -815
  171. dtlpy/repositories/feature_sets.py +256 -226
  172. dtlpy/repositories/features.py +255 -255
  173. dtlpy/repositories/integrations.py +484 -484
  174. dtlpy/repositories/items.py +912 -912
  175. dtlpy/repositories/messages.py +94 -94
  176. dtlpy/repositories/models.py +1000 -1000
  177. dtlpy/repositories/nodes.py +80 -80
  178. dtlpy/repositories/ontologies.py +511 -511
  179. dtlpy/repositories/organizations.py +525 -525
  180. dtlpy/repositories/packages.py +1941 -1941
  181. dtlpy/repositories/pipeline_executions.py +451 -451
  182. dtlpy/repositories/pipelines.py +640 -640
  183. dtlpy/repositories/projects.py +539 -539
  184. dtlpy/repositories/recipes.py +429 -399
  185. dtlpy/repositories/resource_executions.py +137 -137
  186. dtlpy/repositories/schema.py +120 -120
  187. dtlpy/repositories/service_drivers.py +213 -213
  188. dtlpy/repositories/services.py +1704 -1704
  189. dtlpy/repositories/settings.py +339 -339
  190. dtlpy/repositories/tasks.py +1477 -1477
  191. dtlpy/repositories/times_series.py +278 -278
  192. dtlpy/repositories/triggers.py +536 -536
  193. dtlpy/repositories/upload_element.py +257 -257
  194. dtlpy/repositories/uploader.py +661 -661
  195. dtlpy/repositories/webhooks.py +249 -249
  196. dtlpy/services/__init__.py +22 -22
  197. dtlpy/services/aihttp_retry.py +131 -131
  198. dtlpy/services/api_client.py +1786 -1785
  199. dtlpy/services/api_reference.py +40 -40
  200. dtlpy/services/async_utils.py +133 -133
  201. dtlpy/services/calls_counter.py +44 -44
  202. dtlpy/services/check_sdk.py +68 -68
  203. dtlpy/services/cookie.py +115 -115
  204. dtlpy/services/create_logger.py +156 -156
  205. dtlpy/services/events.py +84 -84
  206. dtlpy/services/logins.py +235 -235
  207. dtlpy/services/reporter.py +256 -256
  208. dtlpy/services/service_defaults.py +91 -91
  209. dtlpy/utilities/__init__.py +20 -20
  210. dtlpy/utilities/annotations/__init__.py +16 -16
  211. dtlpy/utilities/annotations/annotation_converters.py +269 -269
  212. dtlpy/utilities/base_package_runner.py +285 -264
  213. dtlpy/utilities/converter.py +1650 -1650
  214. dtlpy/utilities/dataset_generators/__init__.py +1 -1
  215. dtlpy/utilities/dataset_generators/dataset_generator.py +670 -670
  216. dtlpy/utilities/dataset_generators/dataset_generator_tensorflow.py +23 -23
  217. dtlpy/utilities/dataset_generators/dataset_generator_torch.py +21 -21
  218. dtlpy/utilities/local_development/__init__.py +1 -1
  219. dtlpy/utilities/local_development/local_session.py +179 -179
  220. dtlpy/utilities/reports/__init__.py +2 -2
  221. dtlpy/utilities/reports/figures.py +343 -343
  222. dtlpy/utilities/reports/report.py +71 -71
  223. dtlpy/utilities/videos/__init__.py +17 -17
  224. dtlpy/utilities/videos/video_player.py +598 -598
  225. dtlpy/utilities/videos/videos.py +470 -470
  226. {dtlpy-1.115.44.data → dtlpy-1.117.6.data}/scripts/dlp +1 -1
  227. dtlpy-1.117.6.data/scripts/dlp.bat +2 -0
  228. {dtlpy-1.115.44.data → dtlpy-1.117.6.data}/scripts/dlp.py +128 -128
  229. {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/METADATA +186 -186
  230. dtlpy-1.117.6.dist-info/RECORD +239 -0
  231. {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/WHEEL +1 -1
  232. {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/licenses/LICENSE +200 -200
  233. tests/features/environment.py +551 -551
  234. dtlpy/assets/__pycache__/__init__.cpython-310.pyc +0 -0
  235. dtlpy-1.115.44.data/scripts/dlp.bat +0 -2
  236. dtlpy-1.115.44.dist-info/RECORD +0 -240
  237. {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/entry_points.txt +0 -0
  238. {dtlpy-1.115.44.dist-info → dtlpy-1.117.6.dist-info}/top_level.txt +0 -0
@@ -1,1941 +1,1941 @@
1
- import collections.abc
2
- import importlib.util
3
- import inspect
4
- import logging
5
- import hashlib
6
- import typing
7
- import json
8
- import sys
9
- import os
10
- import re
11
- from copy import deepcopy
12
- from shutil import copyfile
13
- from concurrent.futures import ThreadPoolExecutor
14
-
15
- from .. import entities, repositories, exceptions, utilities, miscellaneous, assets, services, _api_reference
16
- from ..services.api_client import ApiClient
17
-
18
- logger = logging.getLogger(name='dtlpy')
19
-
20
- DEFAULT_PACKAGE_METHOD = entities.PackageFunction(name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
21
- description='',
22
- inputs=[],
23
- outputs=[])
24
- DEFAULT_PACKAGE_MODULE = entities.PackageModule(init_inputs=list(),
25
- entry_point=entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
26
- class_name=entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
27
- name=entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
28
- functions=[DEFAULT_PACKAGE_METHOD])
29
-
30
-
31
- class PackageCatalog:
32
- DEFAULT_PACKAGE_TYPE = 'default_package_type'
33
- MULTI_MODULE = 'multi_module'
34
- MULTI_MODULE_WITH_TRIGGER = 'multi_module_with_trigger'
35
- SINGLE_FUNCTION_ITEM = 'single_function_item'
36
- SINGLE_FUNCTION_JSON = 'single_function_json'
37
- SINGLE_FUNCTION_DATASET = 'single_function_dataset'
38
- SINGLE_FUNCTION_ANNOTATION = 'single_function_annotation'
39
- SINGLE_FUNCTION_NO_INPUT = 'single_function_no_input'
40
- SINGLE_FUNCTION_MULTI_INPUT = 'single_function_multi_input'
41
- MULTI_FUNCTION_ITEM = 'multi_function_item'
42
- MULTI_FUNCTION_DATASET = 'multi_function_dataset'
43
- MULTI_FUNCTION_ANNOTATION = 'multi_function_annotation'
44
- MULTI_FUNCTION_NO_INPUT = 'multi_function_no_input'
45
- MULTI_FUNCTION_JSON = 'multi_function_json'
46
- SINGLE_FUNCTION_ITEM_WITH_TRIGGER = 'single_function_item_with_trigger'
47
- SINGLE_FUNCTION_DATASET_WITH_TRIGGER = 'single_function_dataset_with_trigger'
48
- SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER = 'single_function_annotation_with_trigger'
49
- MULTI_FUNCTION_ITEM_WITH_TRIGGERS = 'multi_function_item_with_triggers'
50
- MULTI_FUNCTION_DATASET_WITH_TRIGGERS = 'multi_function_dataset_with_triggers'
51
- MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS = 'multi_function_annotation_with_triggers'
52
- CONVERTER_FUNCTION = 'converter'
53
-
54
-
55
- class Packages:
56
- """
57
- Packages Repository
58
-
59
- The Packages class allows users to manage packages (code used for running in Dataloop's FaaS) and their properties.
60
- Read more about `Packages <https://developers.dataloop.ai/tutorials/faas/introduction/chapter/>`_.
61
- """
62
-
63
- def __init__(self, client_api: ApiClient, project: entities.Project = None):
64
- self._client_api = client_api
65
- self._project = project
66
- self.package_io = PackageIO()
67
-
68
- ############
69
- # entities #
70
- ############
71
- @property
72
- def project(self) -> entities.Project:
73
- if self._project is None:
74
- try:
75
- self._project = repositories.Projects(client_api=self._client_api).get()
76
- except exceptions.NotFound:
77
- raise exceptions.PlatformException(
78
- error='2001',
79
- message='Missing "project". need to set a Project entity or use project.packages repository')
80
- return self._project
81
-
82
- @project.setter
83
- def project(self, project: entities.Project):
84
- if not isinstance(project, entities.Project):
85
- raise ValueError('Must input a valid Project entity')
86
- self._project = project
87
-
88
- ###########
89
- # methods #
90
- ###########
91
- @property
92
- def platform_url(self):
93
- return self._client_api._get_resource_url("projects/{}/packages".format(self.project.id))
94
-
95
- def open_in_web(self,
96
- package: entities.Package = None,
97
- package_id: str = None,
98
- package_name: str = None):
99
- """
100
- Open the package in the web platform.
101
-
102
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
103
-
104
- :param dtlpy.entities.package.Package package: package entity
105
- :param str package_id: package id
106
- :param str package_name: package name
107
-
108
- **Example**:
109
-
110
- .. code-block:: python
111
-
112
- project.packages.open_in_web(package_id='package_id')
113
- """
114
- if package_name is not None:
115
- package = self.get(package_name=package_name)
116
- if package is not None:
117
- package.open_in_web()
118
- elif package_id is not None:
119
- self._client_api._open_in_web(url=self.platform_url + '/' + str(package_id) + '/main')
120
- else:
121
- self._client_api._open_in_web(url=self.platform_url)
122
-
123
- @_api_reference.add(path='/packages/{id}/revisions', method='get')
124
- def revisions(self, package: entities.Package = None, package_id: str = None):
125
- """
126
- Get the package revision history.
127
-
128
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
129
-
130
- :param dtlpy.entities.package.Package package: package entity
131
- :param str package_id: package id
132
-
133
- **Example**:
134
-
135
- .. code-block:: python
136
-
137
- project.packages.revisions(package='package_entity')
138
- """
139
- if package is None and package_id is None:
140
- raise exceptions.PlatformException(
141
- error='400',
142
- message='must provide an identifier in inputs: "package" or "package_id"')
143
- if package is not None:
144
- package_id = package.id
145
-
146
- success, response = self._client_api.gen_request(
147
- req_type="get",
148
- path="/packages/{}/revisions".format(package_id))
149
- if not success:
150
- raise exceptions.PlatformException(response)
151
- return response.json()
152
-
153
- @_api_reference.add(path='/packages/{id}', method='get')
154
- def get(self,
155
- package_name: str = None,
156
- package_id: str = None,
157
- checkout: bool = False,
158
- fetch=None,
159
- log_error=True) -> entities.Package:
160
- """
161
- Get Package object to use in your code.
162
-
163
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
164
-
165
- :param str package_id: package id
166
- :param str package_name: package name
167
- :param bool checkout: set the package as a default package object (cookies)
168
- :param fetch: optional - fetch entity from platform, default taken from cookie
169
- :param bool log_error: log error if package not found
170
- :return: Package object
171
- :rtype: dtlpy.entities.package.Package
172
-
173
- **Example**:
174
-
175
- .. code-block:: python
176
-
177
- project.packages.get(package_id='package_id')
178
- """
179
- if fetch is None:
180
- fetch = self._client_api.fetch_entities
181
-
182
- if package_name is None and package_id is None:
183
- package = self.__get_from_cache()
184
- if package is None:
185
- raise exceptions.PlatformException(
186
- error='400',
187
- message='No checked-out Package was found, must checkout or provide an identifier in inputs')
188
- elif fetch:
189
- if package_id is not None:
190
- success, response = self._client_api.gen_request(
191
- req_type="get",
192
- path="/packages/{}".format(package_id),
193
- log_error=log_error)
194
- if not success:
195
- raise exceptions.PlatformException(response)
196
- package = entities.Package.from_json(client_api=self._client_api,
197
- _json=response.json(),
198
- project=self._project)
199
- # verify input package name is same as the given id
200
- if package_name is not None and package.name != package_name:
201
- logger.warning(
202
- "Mismatch found in packages.get: package_name is different then package.name:"
203
- " {!r} != {!r}".format(
204
- package_name,
205
- package.name))
206
- elif package_name is not None:
207
- filters = entities.Filters(field='name', values=package_name, resource=entities.FiltersResource.PACKAGE,
208
- use_defaults=False)
209
- if self._project is not None:
210
- filters.add(field='projectId', values=self._project.id)
211
- packages = self.list(filters=filters)
212
- if packages.items_count == 0:
213
- raise exceptions.PlatformException(
214
- error='404',
215
- message='Package not found. Name: {}'.format(package_name))
216
- elif packages.items_count > 1:
217
- raise exceptions.PlatformException(
218
- error='400',
219
- message='More than one package found by the name of: {} '
220
- 'Please get package from a project entity'.format(package_name))
221
- package = packages.items[0]
222
- else:
223
- raise exceptions.PlatformException(
224
- error='400',
225
- message='No checked-out Package was found, must checkout or provide an identifier in inputs')
226
- else:
227
- package = entities.Package.from_json(_json={'id': package_id,
228
- 'name': package_name},
229
- client_api=self._client_api,
230
- project=self._project,
231
- is_fetched=False)
232
-
233
- if checkout:
234
- self.checkout(package=package)
235
- return package
236
-
237
- def _build_entities_from_response(self, response_items) -> miscellaneous.List[entities.Package]:
238
- pool = self._client_api.thread_pools(pool_name='entity.create')
239
- jobs = [None for _ in range(len(response_items))]
240
- # return triggers list
241
- for i_package, package in enumerate(response_items):
242
- jobs[i_package] = pool.submit(entities.Package._protected_from_json,
243
- **{'client_api': self._client_api,
244
- '_json': package,
245
- 'project': self._project})
246
-
247
- # get all results
248
- results = [j.result() for j in jobs]
249
- # log errors
250
- _ = [logger.warning(r[1]) for r in results if r[0] is False]
251
- # return good jobs
252
- packages = miscellaneous.List([r[1] for r in results if r[0] is True])
253
- return packages
254
-
255
- def _list(self, filters: entities.Filters):
256
- url = '/query/faas'
257
-
258
- # request
259
- success, response = self._client_api.gen_request(req_type='post',
260
- path=url,
261
- json_req=filters.prepare())
262
- if not success:
263
- raise exceptions.PlatformException(response)
264
- return response.json()
265
-
266
- @_api_reference.add(path='/query/faas', method='post')
267
- def list(self, filters: entities.Filters = None, project_id: str = None) -> entities.PagedEntities:
268
- """
269
- List project packages.
270
-
271
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
272
-
273
- :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
274
- :param str project_id: project id
275
- :return: Paged entity
276
- :rtype: dtlpy.entities.paged_entities.PagedEntities
277
-
278
- **Example**:
279
-
280
- .. code-block:: python
281
-
282
- project.packages.list()
283
- """
284
- if filters is None:
285
- filters = entities.Filters(resource=entities.FiltersResource.PACKAGE)
286
- # assert type filters
287
- elif not isinstance(filters, entities.Filters):
288
- raise exceptions.PlatformException(error='400',
289
- message='Unknown filters type: {!r}'.format(type(filters)))
290
- if filters.resource != entities.FiltersResource.PACKAGE:
291
- raise exceptions.PlatformException(
292
- error='400',
293
- message='Filters resource must to be FiltersResource.PACKAGE. Got: {!r}'.format(filters.resource))
294
-
295
- if project_id is None and self._project is not None:
296
- project_id = self._project.id
297
-
298
- if project_id is not None:
299
- filters.add(field='projectId', values=project_id)
300
-
301
- paged = entities.PagedEntities(items_repository=self,
302
- filters=filters,
303
- page_offset=filters.page,
304
- page_size=filters.page_size,
305
- project_id=project_id,
306
- client_api=self._client_api)
307
- paged.get_page()
308
- return paged
309
-
310
- def pull(self, package: entities.Package, version=None, local_path=None, project_id=None):
311
- """
312
- Pull (download) the package to a local path.
313
-
314
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
315
-
316
- :param dtlpy.entities.package.Package package: package entity
317
- :param str version: the package version to pull
318
- :param local_path: the path of where to save the package
319
- :param project_id: the project id that include the package
320
- :return: local path where the package pull
321
- :rtype: str
322
-
323
- **Example**:
324
-
325
- .. code-block:: python
326
-
327
- project.packages.pull(package='package_entity', local_path='local_path')
328
- """
329
- if isinstance(version, int):
330
- logger.warning('Deprecation Warning - Package/service versions have been refactored'
331
- 'The version you provided has type: int, it will be converted to: 1.0.{}'
332
- 'Next time use a 3-level semver for package/service versions'.format(version))
333
-
334
- dir_version = version
335
- if version is None:
336
- dir_version = package.version
337
-
338
- if local_path is None:
339
- local_path = os.path.join(
340
- services.service_defaults.DATALOOP_PATH,
341
- "packages",
342
- "{}-{}".format(package.name, package.id),
343
- str(dir_version))
344
-
345
- if version is None or version == package.version:
346
- package_revision = package
347
- else:
348
- versions = [revision for revision in package.revisions if revision['version'] == version]
349
- if len(versions) <= 0:
350
- raise exceptions.PlatformException('404', 'Version not found: version={}'.format(version))
351
- elif len(versions) > 1:
352
- raise exceptions.PlatformException('404', 'More than one version found: version={}'.format(version))
353
- package_revision = entities.Package.from_json(
354
- _json=versions[0],
355
- project=package.project,
356
- client_api=package._client_api
357
- )
358
- if package_revision.codebase.type == entities.PackageCodebaseType.ITEM:
359
- local_path = package_revision.codebases.unpack(codebase_id=package_revision.codebase.item_id,
360
- local_path=local_path)
361
- elif package_revision.codebase.type == entities.PackageCodebaseType.GIT:
362
- local_path = package_revision.codebases.unpack(codebase=package_revision.codebase,
363
- local_path=local_path)
364
- else:
365
- raise Exception(
366
- 'Pull can only be performed on packages with Item or Git codebase. Codebase type: {!r}'.format(
367
- package_revision.codebase.type))
368
-
369
- return local_path
370
-
371
- def _name_validation(self, name: str):
372
- url = '/piper-misc/naming/packages/{}'.format(name)
373
-
374
- # request
375
- success, response = self._client_api.gen_request(req_type='get',
376
- path=url)
377
- if not success:
378
- raise exceptions.PlatformException(response)
379
-
380
- @staticmethod
381
- def _validate_slots(slots, modules):
382
- for slot in slots:
383
- matched_module = [module for module in modules if slot.module_name == module.name]
384
- if len(matched_module) != 1:
385
- raise ValueError('Module {!r} in slots is not defined in modules.'.format(slot.module_name))
386
- matched_func = [func for func in matched_module[0].functions if slot.function_name == func.name]
387
- if len(matched_func) != 1:
388
- raise ValueError('Function {!r} in slots is not defined in module {!r}.'.format(slot.function_name,
389
- slot.module_name))
390
-
391
- def build_requirements(self, filepath) -> list:
392
- """
393
- Build a requirement list (list of packages your code requires to run) from a file path. **The file listing the requirements MUST BE a txt file**.
394
-
395
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
396
-
397
- :param filepath: path of the requirements file
398
- :return: a list of dl.PackageRequirement
399
- :rtype: list
400
- """
401
- requirements_list = []
402
- if not os.path.exists(filepath):
403
- raise exceptions.PlatformException('400', 'requirements file {} does not exist'.format(filepath))
404
- with open(filepath) as requirements_txt:
405
- for requirement in requirements_txt:
406
- requirement = requirement.strip()
407
- if '\n' in requirement:
408
- requirement.replace('\n', '')
409
- if requirement.startswith('#'):
410
- continue
411
- elif ' #' in requirement:
412
- requirement = requirement[:requirement.find(' #')]
413
- if 'install -r' in requirement:
414
- requirements_list.extend(self.build_requirements(requirement[requirement.find('-r ') + 3:]))
415
- continue
416
- requirement_spit = requirement.split(',')
417
- for req in requirement_spit:
418
- name_version = re.split('<=|>=|==|<|>', req)
419
- req_name = name_version[0]
420
- req_version, op = None, None
421
- if len(name_version) > 1:
422
- op = re.findall('<=|>=|==|<|>', req)[0]
423
- req_version = name_version[1]
424
- if req_name == '':
425
- req_name = last_req_name
426
- last_req_name = req_name
427
- requirements_list.append(
428
- entities.PackageRequirement(name=req_name, version=req_version, operator=op))
429
- return requirements_list
430
-
431
- @_api_reference.add(path='/packages', method='post')
432
- def push(self,
433
- project: entities.Project = None,
434
- project_id: str = None,
435
- package_name: str = None,
436
- src_path: str = None,
437
- codebase: typing.Union[entities.GitCodebase, entities.ItemCodebase, entities.FilesystemCodebase] = None,
438
- modules: typing.List[entities.PackageModule] = None,
439
- is_global: bool = None,
440
- checkout: bool = False,
441
- revision_increment: str = None,
442
- version: str = None,
443
- ignore_sanity_check: bool = False,
444
- service_update: bool = False,
445
- service_config: dict = None,
446
- slots: typing.List[entities.PackageSlot] = None,
447
- requirements: typing.List[entities.PackageRequirement] = None,
448
- package_type=None,
449
- metadata=None
450
- ) -> entities.Package:
451
- """
452
- Push your local package to the UI.
453
-
454
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
455
-
456
- Project will be taken in the following hierarchy:
457
- project(input) -> project_id(input) -> self.project(context) -> checked out
458
-
459
- :param dtlpy.entities.project.Project project: optional - project entity to deploy to. default from context or checked-out
460
- :param str project_id: optional - project id to deploy to. default from context or checked-out
461
- :param str package_name: package name
462
- :param str src_path: path to package codebase
463
- :param dtlpy.entities.codebase.Codebase codebase: codebase object
464
- :param list modules: list of modules PackageModules of the package
465
- :param bool is_global: is package is global or local
466
- :param bool checkout: checkout package to local dir
467
- :param str revision_increment: optional - str - version bumping method - major/minor/patch - default = None
468
- :param str version: semver version f the package
469
- :param bool ignore_sanity_check: NOT RECOMMENDED - skip code sanity check before pushing
470
- :param bool service_update: optional - bool - update the service
471
- :param dict service_config : Service object as dict. Contains the spec of the default service to create.
472
- :param list slots: optional - list of slots PackageSlot of the package
473
- :param list requirements: requirements - list of package requirements
474
- :param str package_type: default 'faas', options: 'app', 'ml
475
- :param dict metadata: dictionary of system and user metadata
476
-
477
- :return: Package object
478
- :rtype: dtlpy.entities.package.Package
479
-
480
- **Example**:
481
-
482
- .. code-block:: python
483
-
484
- project.packages.push(package_name='package_name',
485
- modules=[module],
486
- version='1.0.0',
487
- src_path=os.getcwd()
488
- )
489
- """
490
- # get project
491
- project_to_deploy = None
492
- if project is not None:
493
- project_to_deploy = project
494
- elif project_id is not None:
495
- project_to_deploy = repositories.Projects(client_api=self._client_api).get(project_id=project_id)
496
- elif self._project is not None:
497
- project_to_deploy = self._project
498
- else:
499
- try:
500
- project_to_deploy = repositories.Projects(client_api=self._client_api).get()
501
- except Exception:
502
- pass
503
-
504
- if project_to_deploy is None:
505
- raise exceptions.PlatformException(
506
- error='400',
507
- message='Missing project from "packages.push" function. '
508
- 'Please provide project or id, use Packages from a '
509
- 'project.packages repository or checkout a project')
510
-
511
- # source path
512
- if src_path is None:
513
- if codebase is None:
514
- src_path = os.getcwd()
515
- logger.warning('No src_path is given, getting package information from cwd: {}'.format(src_path))
516
-
517
- # get package json
518
- package_from_json = dict()
519
- path_files = os.listdir(src_path) if src_path is not None else []
520
- if assets.paths.PACKAGE_FILENAME in path_files:
521
- with open(os.path.join(src_path, assets.paths.PACKAGE_FILENAME), 'r') as f:
522
- package_from_json = json.load(f)
523
-
524
- if requirements is not None and not isinstance(requirements, list):
525
- requirements = [requirements]
526
-
527
- if requirements and assets.paths.REQUIREMENTS_FILENAME in path_files:
528
- logger.warning('Have both requirements param and requirements file will overwrite the requirements file')
529
-
530
- if not requirements and assets.paths.REQUIREMENTS_FILENAME in path_files:
531
- req_path = os.path.join(src_path, assets.paths.REQUIREMENTS_FILENAME)
532
- req_from_file = self.build_requirements(filepath=req_path)
533
- requirements = req_from_file
534
-
535
- # get name
536
- if package_name is None:
537
- package_name = package_from_json.get('name', 'default-package')
538
-
539
- if modules is None and 'modules' in package_from_json:
540
- modules = package_from_json['modules']
541
-
542
- if slots is None and 'slots' in package_from_json:
543
- slots = package_from_json['slots']
544
-
545
- if ignore_sanity_check:
546
- logger.warning(
547
- 'Pushing a package without sanity check can cause errors when trying to deploy, '
548
- 'trigger and execute functions.\n'
549
- 'We highly recommend to not use the ignore_sanity_check flag')
550
- elif codebase is None:
551
- modules = self._sanity_before_push(src_path=src_path, modules=modules)
552
-
553
- self._name_validation(name=package_name)
554
-
555
- if slots is not None:
556
- if modules is None:
557
- raise ValueError('Cannot add slots when modules is empty.')
558
- if not isinstance(slots[0], entities.PackageSlot):
559
- slots = [entities.PackageSlot.from_json(_json=slot) for slot in slots]
560
- self._validate_slots(slots, modules)
561
-
562
- delete_codebase = False
563
- if codebase is None:
564
- codebase = project_to_deploy.codebases.pack(directory=src_path, name=package_name)
565
- delete_codebase = True
566
-
567
- try:
568
- # check if exist
569
- filters = entities.Filters(resource=entities.FiltersResource.PACKAGE, use_defaults=False)
570
- filters.add(field='projectId', values=project_to_deploy.id)
571
- filters.add(field='name', values=package_name)
572
- packages = self.list(filters=filters)
573
- if packages.items_count > 0:
574
- # package exists - need to update
575
- package = packages.items[0]
576
-
577
- if modules is not None:
578
- package.modules = modules
579
-
580
- if slots is not None:
581
- package.slots = slots
582
-
583
- if is_global is not None:
584
- package.is_global = is_global
585
-
586
- if codebase is not None:
587
- package.codebase = codebase
588
-
589
- if version is not None:
590
- package.version = version
591
-
592
- if requirements is not None:
593
- package.requirements = requirements
594
-
595
- if service_config is not None:
596
- package.service_config = service_config
597
-
598
- if metadata is not None:
599
- package.metadata = metadata
600
-
601
- if package_type is not None:
602
- package.package_type = package_type
603
- package = self.update(package=package, revision_increment=revision_increment)
604
- else:
605
- package = self._create(
606
- project_to_deploy=project_to_deploy,
607
- package_name=package_name,
608
- modules=modules,
609
- slots=slots,
610
- codebase=codebase,
611
- is_global=is_global,
612
- version=version,
613
- service_config=service_config,
614
- requirements=requirements,
615
- package_type=package_type,
616
- metadata=metadata
617
- )
618
- if checkout:
619
- self.checkout(package=package)
620
- except Exception:
621
- if delete_codebase:
622
- project_to_deploy.items.delete(item_id=codebase.item_id)
623
- raise
624
-
625
- logger.info("Package name: {!r}, id: {!r} has been added to project name: {!r}, id: {!r}".format(
626
- package.name,
627
- package.id,
628
- package.project.name,
629
- package.project.id))
630
-
631
- if service_update:
632
- service = package.services.get(service_name=package.name)
633
- service.package_revision = package.version
634
- service.update()
635
- return package
636
-
637
- def _create(self,
638
- project_to_deploy: entities.Project = None,
639
- codebase: typing.Union[entities.GitCodebase, entities.ItemCodebase, entities.FilesystemCodebase] = None,
640
- is_global: bool = None,
641
- package_name: str = entities.package_defaults.DEFAULT_PACKAGE_NAME,
642
- modules: typing.List[entities.PackageModule] = None,
643
- version: str = None,
644
- service_config: dict = None,
645
- slots: typing.List[entities.PackageSlot] = None,
646
- requirements: typing.List[entities.PackageRequirement] = None,
647
- package_type='faas',
648
- metadata=None
649
- ) -> entities.Package:
650
- """
651
- Create a package in platform.
652
-
653
- :param project_to_deploy:
654
- :param dtlpy.entities.codebase.Codebase codebase: codebase object
655
- :param bool is_global: is package is global or local
656
- :param str package_name: optional - default: 'default package'
657
- :param list modules: optional - PackageModules Entity
658
- :param str version: semver version of the package
659
- :param dict service_config : Service object as dict. Contains the spec of the default service to create.
660
- :param list slots: optional - list of slots PackageSlot of the package
661
- :param list requirements: requirements - list of package requirements
662
- :param str package_type: default 'faas', options: 'app', 'ml
663
- :param dict metadata: dictionary of system and user metadata
664
- :return: Package object
665
- :rtype: dtlpy.entities.package.Package
666
- """
667
- if modules is not None:
668
- if not isinstance(modules, list):
669
- modules = [modules]
670
-
671
- if isinstance(modules[0], entities.PackageModule):
672
- modules = [module.to_json() for module in modules]
673
-
674
- if slots and isinstance(slots[0], entities.PackageSlot):
675
- slots = [slot.to_json() for slot in slots]
676
-
677
- if is_global is None:
678
- is_global = False
679
-
680
- payload = {'name': package_name,
681
- 'global': is_global,
682
- 'modules': modules,
683
- 'slots': slots,
684
- 'serviceConfig': service_config,
685
- 'type': package_type
686
- }
687
-
688
- if codebase is not None:
689
- payload['codebase'] = codebase.to_json()
690
-
691
- if requirements is not None:
692
- payload['requirements'] = [r.to_json() for r in requirements]
693
-
694
- if version is not None:
695
- payload['version'] = version
696
-
697
- if metadata is not None:
698
- payload['metadata'] = metadata
699
-
700
- if project_to_deploy is not None:
701
- payload['projectId'] = project_to_deploy.id
702
- else:
703
- raise exceptions.PlatformException('400', 'Repository must have a project to perform this action')
704
-
705
- # request
706
- success, response = self._client_api.gen_request(req_type='post',
707
- path='/packages',
708
- json_req=payload)
709
-
710
- # exception handling
711
- if not success:
712
- raise exceptions.PlatformException(response)
713
-
714
- # return entity
715
- return entities.Package.from_json(_json=response.json(),
716
- client_api=self._client_api,
717
- project=project_to_deploy)
718
-
719
- @_api_reference.add(path='/packages/{id}', method='delete')
720
- def delete(self, package: entities.Package = None, package_name=None, package_id=None):
721
- """
722
- Delete a Package object.
723
-
724
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
725
-
726
- :param dtlpy.entities.package.Package package: package entity
727
- :param str package_id: package id
728
- :param str package_name: package name
729
- :return: True if success
730
- :rtype: bool
731
-
732
- **Example**:
733
-
734
- .. code-block:: python
735
-
736
- project.packages.delete(package_name='package_name')
737
- """
738
- # get id and name
739
- if package_name is None or package_id is None:
740
- if package is None:
741
- package = self.get(package_id=package_id, package_name=package_name)
742
- package_id = package.id
743
-
744
- # check if project exist
745
- project_exists = True
746
- if self._project is None:
747
- try:
748
- if package is not None and package.project is not None:
749
- self._project = package.project
750
- else:
751
- self._project = repositories.Projects(client_api=self._client_api).get(
752
- project_id=package.project_id)
753
- except exceptions.NotFound:
754
- project_exists = False
755
-
756
- if project_exists:
757
- # TODO can remove? in transactor?
758
- try:
759
- # create codebases repo
760
- codebases = repositories.Codebases(client_api=self._client_api, project=self._project)
761
- # get package codebases
762
- codebase_pages = codebases.list_versions(codebase_name=package_name)
763
- for codebase_page in codebase_pages:
764
- for codebase in codebase_page:
765
- codebase.delete()
766
- except exceptions.Forbidden:
767
- logger.debug('Failed to delete code-bases. Continue without')
768
-
769
- # request
770
- success, response = self._client_api.gen_request(
771
- req_type="delete",
772
- path="/packages/{}".format(package_id)
773
- )
774
-
775
- # exception handling
776
- if not success:
777
- raise exceptions.PlatformException(response)
778
-
779
- # return results
780
- return True
781
-
782
- @_api_reference.add(path='/packages/{id}', method='patch')
783
- def update(self, package: entities.Package, revision_increment: str = None) -> entities.Package:
784
- """
785
- Update Package changes to the platform.
786
-
787
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
788
-
789
- :param dtlpy.entities.package.Package package:
790
- :param revision_increment: optional - str - version bumping method - major/minor/patch - default = None
791
- :return: Package object
792
- :rtype: dtlpy.entities.package.Package
793
-
794
- **Example**:
795
-
796
- .. code-block:: python
797
-
798
- project.packages.delete(package='package_entity')
799
- """
800
-
801
- if revision_increment is not None and isinstance(package.version, str) and len(package.version.split('.')) == 3:
802
- major, minor, patch = package.version.split('.')
803
- if revision_increment == 'patch':
804
- patch = int(patch) + 1
805
- elif revision_increment == 'minor':
806
- minor = int(minor) + 1
807
- elif revision_increment == 'major':
808
- major = int(major) + 1
809
- package.version = '{}.{}.{}'.format(major, minor, patch)
810
-
811
- # payload
812
- payload = package.to_json()
813
-
814
- # request
815
- success, response = self._client_api.gen_request(req_type='patch',
816
- path='/packages/{}'.format(package.id),
817
- json_req=payload)
818
-
819
- # exception handling
820
- if not success:
821
- raise exceptions.PlatformException(response)
822
-
823
- # return entity
824
- return entities.Package.from_json(_json=response.json(),
825
- client_api=self._client_api,
826
- project=self._project)
827
-
828
- def deploy(self,
829
- package_id: str = None,
830
- package_name: str = None,
831
- package: entities.Package = None,
832
- service_name: str = None,
833
- project_id: str = None,
834
- revision: str or int = None,
835
- init_input: typing.Union[typing.List[entities.FunctionIO], entities.FunctionIO, dict] = None,
836
- runtime: typing.Union[entities.KubernetesRuntime, dict] = None,
837
- sdk_version: str = None,
838
- agent_versions: dict = None,
839
- bot: typing.Union[entities.Bot, str] = None,
840
- pod_type: entities.InstanceCatalog = None,
841
- verify: bool = True,
842
- checkout: bool = False,
843
- module_name: str = None,
844
- run_execution_as_process: bool = None,
845
- execution_timeout: int = None,
846
- drain_time: int = None,
847
- on_reset: str = None,
848
- max_attempts: int = None,
849
- force: bool = False,
850
- secrets: list = None,
851
- **kwargs) -> entities.Service:
852
- """
853
- Deploy a package. A service is required to run the code in your package.
854
-
855
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
856
-
857
- :param str package_id: package id
858
- :param str package_name: package name
859
- :param dtlpy.entities.package.Package package: package entity
860
- :param str service_name: service name
861
- :param str project_id: project id
862
- :param str revision: package revision - default=latest
863
- :param init_input: config to run at startup
864
- :param dict runtime: runtime resources
865
- :param str sdk_version: - optional - string - sdk version
866
- :param dict agent_versions: - dictionary - - optional -versions of sdk, agent runner and agent proxy
867
- :param str bot: bot email
868
- :param str pod_type: pod type dl.InstanceCatalog
869
- :param bool verify: verify the inputs
870
- :param bool checkout: checkout
871
- :param str module_name: module name
872
- :param bool run_execution_as_process: run execution as process
873
- :param int execution_timeout: execution timeout
874
- :param int drain_time: drain time
875
- :param str on_reset: on reset
876
- :param int max_attempts: Maximum execution retries in-case of a service reset
877
- :param bool force: optional - terminate old replicas immediately
878
- :param list secrets: list of the integrations ids
879
- :return: Service object
880
- :rtype: dtlpy.entities.service.Service
881
-
882
- **Example**:
883
-
884
- .. code-block:: python
885
-
886
- project.packages.deploy(service_name=package_name,
887
- execution_timeout=3 * 60 * 60,
888
- module_name=module.name,
889
- runtime=dl.KubernetesRuntime(
890
- concurrency=10,
891
- pod_type=dl.InstanceCatalog.REGULAR_S,
892
- autoscaler=dl.KubernetesRabbitmqAutoscaler(
893
- min_replicas=1,
894
- max_replicas=20,
895
- queue_length=20
896
- )
897
- )
898
- )
899
- """
900
-
901
- if package is None:
902
- package = self.get(package_id=package_id, package_name=package_name)
903
-
904
- return package.services.deploy(package=package,
905
- service_name=service_name,
906
- revision=revision,
907
- init_input=init_input,
908
- runtime=runtime,
909
- sdk_version=sdk_version,
910
- agent_versions=agent_versions,
911
- project_id=project_id,
912
- pod_type=pod_type,
913
- bot=bot,
914
- verify=verify,
915
- module_name=module_name,
916
- checkout=checkout,
917
- jwt_forward=kwargs.get('jwt_forward', None),
918
- is_global=kwargs.get('is_global', None),
919
- run_execution_as_process=run_execution_as_process,
920
- execution_timeout=execution_timeout,
921
- drain_time=drain_time,
922
- on_reset=on_reset,
923
- max_attempts=max_attempts,
924
- force=force,
925
- secrets=secrets
926
- )
927
-
928
- def deploy_from_file(self, project, json_filepath):
929
- """
930
- Deploy package and service from a JSON file.
931
-
932
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
933
-
934
- :param dtlpy.entities.project.Project project: project entity
935
- :param str json_filepath: path of the file to deploy
936
- :return: the package and the services
937
-
938
- **Example**:
939
-
940
- .. code-block:: python
941
-
942
- project.packages.deploy_from_file(project='project_entity', json_filepath='json_filepath')
943
- """
944
- with open(json_filepath, 'r') as f:
945
- data = json.load(f)
946
-
947
- package = project.packages.push(
948
- package_name=data['name'],
949
- modules=data['modules'],
950
- src_path=os.path.split(json_filepath)[0]
951
- )
952
- deployed_services = list()
953
- if 'services' in data:
954
- for service_json in data['services']:
955
- try:
956
- # update
957
- service_old = project.services.get(service_name=service_json['name'])
958
- except exceptions.NotFound:
959
- # create
960
- service_old = package.services.deploy(service_name=service_json['name'],
961
- module_name=data['modules'][0]['name'])
962
- triggers = service_json.pop('triggers', None)
963
- artifacts = service_json.pop('artifacts', None)
964
- to_update, service = self.__compare_and_update_service_configurations(service=service_old,
965
- service_json=service_json)
966
- if to_update:
967
- service.package_revision = package.version
968
- service.update()
969
- if triggers:
970
- self.__compare_and_update_trigger_configurations(service, triggers)
971
- if artifacts:
972
- self.__compare_and_upload_artifacts(artifacts, package)
973
- deployed_services.append(service)
974
- else:
975
- logger.warning(msg='Package JSON does not have services.')
976
- return deployed_services, package
977
-
978
- def __update_dict_recursive(self, d, u):
979
- for k, v in u.items():
980
- if isinstance(v, collections.abc.Mapping):
981
- inner_dict = d.get(k, {})
982
- if inner_dict is None:
983
- inner_dict = {}
984
- d[k] = self.__update_dict_recursive(inner_dict, v)
985
- else:
986
- d[k] = v
987
- return d
988
-
989
- def __compare_and_update_trigger_configurations(self, service, trigger_list):
990
- """
991
- :param service:
992
- :param trigger_list:
993
- """
994
- service_triggers = service.triggers.list().all()
995
- triggers_dict = dict()
996
- for trigger in service_triggers:
997
- triggers_dict[trigger.name] = trigger
998
-
999
- for trigger_json in trigger_list:
1000
- trigger_name = trigger_json['name']
1001
- trigger_spec = trigger_json['spec']
1002
- if trigger_name not in triggers_dict:
1003
- # create
1004
- service.triggers.create(
1005
- # general
1006
- name=trigger_name,
1007
- function_name=trigger_spec['operation']['functionName'],
1008
- trigger_type=trigger_json['type'],
1009
- # event
1010
- scope=trigger_json.get('scope', None),
1011
- is_global=trigger_json.get('global', None),
1012
- filters=trigger_spec.get('filter', None),
1013
- resource=trigger_spec.get('resource', None),
1014
- execution_mode=trigger_spec.get('executionMode', None),
1015
- actions=trigger_spec.get('actions', None),
1016
- # cron
1017
- start_at=trigger_spec.get('startAt', None),
1018
- end_at=trigger_spec.get('endAt', None),
1019
- cron=trigger_spec.get('cron', None),
1020
- )
1021
- else:
1022
- existing_trigger = triggers_dict[trigger_name]
1023
- # check diff
1024
- _json = existing_trigger.to_json()
1025
- # pop unmatched fields
1026
- _ = _json['spec']['operation'].pop('serviceId')
1027
- _new_json = _json.copy()
1028
- # update fields from infra configurations
1029
- self.__update_dict_recursive(_new_json, trigger_json)
1030
- # check diffs
1031
- diffs = miscellaneous.DictDiffer.diff(_json, _new_json)
1032
- if diffs:
1033
- updated_trigger = existing_trigger.from_json(_json=_new_json,
1034
- client_api=service._client_api,
1035
- service=service,
1036
- project=service._project)
1037
- updated_trigger.update()
1038
-
1039
- def __compare_and_update_service_configurations(self, service, service_json):
1040
- """
1041
- :param service:
1042
- :param service_json:
1043
- """
1044
- # take json configuration from service
1045
- _json = service.to_json()
1046
- _new_json = _json.copy()
1047
- # update fields from infra configurations
1048
- self.__update_dict_recursive(_new_json, service_json)
1049
- # check diffs
1050
- diffs = miscellaneous.DictDiffer.diff(_json, _new_json)
1051
- to_update = False
1052
- if diffs:
1053
- # build back service from json
1054
- to_update = True
1055
- service = service.from_json(_json=_new_json,
1056
- client_api=service._client_api,
1057
- package=service._package,
1058
- project=service._project)
1059
- return to_update, service
1060
-
1061
- def __compare_and_upload_artifacts(self, artifacts, package):
1062
- """
1063
- :param artifacts:
1064
- :param package:
1065
- """
1066
- for artifact in artifacts:
1067
- # create/get .dataloop dir
1068
- cwd = os.getcwd()
1069
- dl_dir = os.path.join(cwd, '.dataloop')
1070
- if not os.path.isdir(dl_dir):
1071
- os.mkdir(dl_dir)
1072
-
1073
- directory = os.path.abspath(artifact)
1074
- m = hashlib.md5()
1075
- with open(directory, 'rb') as f:
1076
- for chunk in iter(lambda: f.read(4096), b''):
1077
- m.update(chunk)
1078
- zip_md = m.hexdigest()
1079
- artifacts_list = package.artifacts.list()
1080
- for art in artifacts_list:
1081
- if art.metadata['system']['md5'] == zip_md:
1082
- return
1083
-
1084
- artifact_item = package.artifacts.upload(filepath=directory,
1085
- package_name=package.name,
1086
- package=package,
1087
- overwrite=True, )
1088
-
1089
- artifact_item.metadata['system']['md5'] = zip_md
1090
- artifact_item.update(True)
1091
-
1092
- @staticmethod
1093
- def _package_json_generator(package_catalog, package_name):
1094
- """
1095
- :param package_catalog:
1096
- :param package_name:
1097
- """
1098
- if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1099
- with open(assets.paths.ASSETS_PACKAGE_FILEPATH, 'r') as f:
1100
- package_asset = json.load(f)
1101
- package_asset['name'] = package_name
1102
- return package_asset
1103
-
1104
- item_input = entities.FunctionIO(name='item', type='Item')
1105
- annotation_input = entities.FunctionIO(name='annotation', type='Annotation')
1106
- dataset_input = entities.FunctionIO(name='dataset', type='Dataset')
1107
- json_input = entities.FunctionIO(name='config', type='Json')
1108
- query_input = entities.FunctionIO(name='query', type='Json')
1109
-
1110
- func = entities.PackageFunction(name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME)
1111
- if package_catalog in [PackageCatalog.SINGLE_FUNCTION_ITEM, PackageCatalog.SINGLE_FUNCTION_ITEM_WITH_TRIGGER]:
1112
- func.inputs = [item_input]
1113
- modules = entities.PackageModule(functions=[func])
1114
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ANNOTATION,
1115
- PackageCatalog.SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER]:
1116
- func.inputs = [annotation_input]
1117
- modules = entities.PackageModule(functions=[func])
1118
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_DATASET,
1119
- PackageCatalog.SINGLE_FUNCTION_DATASET_WITH_TRIGGER]:
1120
- func.inputs = [dataset_input]
1121
- modules = entities.PackageModule(functions=[func])
1122
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_NO_INPUT]:
1123
- modules = entities.PackageModule(functions=[func])
1124
- elif package_catalog == PackageCatalog.SINGLE_FUNCTION_JSON:
1125
- func.inputs = json_input
1126
- modules = entities.PackageModule(functions=[func])
1127
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ITEM, PackageCatalog.MULTI_FUNCTION_ITEM_WITH_TRIGGERS]:
1128
- func.inputs = [item_input]
1129
- func.name = 'first_method'
1130
- second_func = deepcopy(func)
1131
- second_func.name = 'second_method'
1132
- modules = entities.PackageModule(functions=[func, second_func])
1133
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ANNOTATION,
1134
- PackageCatalog.MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS]:
1135
- func.inputs = [annotation_input]
1136
- func.name = 'first_method'
1137
- second_func = deepcopy(func)
1138
- second_func.name = 'second_method'
1139
- modules = entities.PackageModule(functions=[func, second_func])
1140
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_DATASET,
1141
- PackageCatalog.MULTI_FUNCTION_DATASET_WITH_TRIGGERS]:
1142
- func.inputs = [dataset_input]
1143
- func.name = 'first_method'
1144
- second_func = deepcopy(func)
1145
- second_func.name = 'second_method'
1146
- modules = entities.PackageModule(functions=[func, second_func])
1147
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_JSON]:
1148
- func.inputs = [json_input]
1149
- func.name = 'first_method'
1150
- second_func = deepcopy(func)
1151
- second_func.name = 'second_method'
1152
- modules = entities.PackageModule(functions=[func, second_func])
1153
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_NO_INPUT]:
1154
- func.name = 'first_method'
1155
- second_func = deepcopy(func)
1156
- second_func.name = 'second_method'
1157
- modules = entities.PackageModule(functions=[func, second_func])
1158
- elif package_catalog in [PackageCatalog.MULTI_MODULE, PackageCatalog.MULTI_MODULE_WITH_TRIGGER]:
1159
- func.inputs = [item_input]
1160
- module_a = entities.PackageModule(functions=[func],
1161
- name='first_module',
1162
- entry_point='first_module_class.py')
1163
- module_b = entities.PackageModule(functions=[func],
1164
- name='second_module',
1165
- entry_point='second_module_class.py')
1166
- modules = [module_a, module_b]
1167
- elif package_catalog == PackageCatalog.SINGLE_FUNCTION_MULTI_INPUT:
1168
- func.inputs = [item_input, dataset_input, json_input, annotation_input]
1169
- modules = entities.PackageModule(functions=[func])
1170
- elif package_catalog == PackageCatalog.CONVERTER_FUNCTION:
1171
- func.inputs = [dataset_input, query_input]
1172
- modules = entities.PackageModule(functions=[func])
1173
- else:
1174
- raise exceptions.PlatformException('404', 'Unknown package catalog type: {}'.format(package_catalog))
1175
-
1176
- if not isinstance(modules, list):
1177
- modules = [modules]
1178
-
1179
- _json = {'name': package_name,
1180
- 'modules': [module.to_json() for module in modules]}
1181
-
1182
- return _json
1183
-
1184
- @staticmethod
1185
- def build_trigger_dict(actions,
1186
- name='default_module',
1187
- filters=None,
1188
- function='run',
1189
- execution_mode: entities.TriggerExecutionMode = 'Once',
1190
- type_t: entities.TriggerType = "Event"):
1191
- """
1192
- Build a trigger dictionary to trigger FaaS.
1193
- Read more about `FaaS Triggers <https://developers.dataloop.ai/tutorials/faas/auto_annotate/chapter/#trigger-the-service>`_.
1194
-
1195
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
1196
-
1197
- :param actions: list of dl.TriggerAction
1198
- :param str name: trigger name
1199
- :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
1200
- :param str function: function name
1201
- :param str execution_mode: execution mode dl.TriggerExecutionMode
1202
- :param str type_t: trigger type dl.TriggerType
1203
- :return: trigger dict
1204
- :rtype: dict
1205
-
1206
- **Example**:
1207
-
1208
- .. code-block:: python
1209
-
1210
- project.packages.build_trigger_dict(actions=dl.TriggerAction.CREATED,
1211
- function='run',
1212
- execution_mode=dl.TriggerExecutionMode.ONCE)
1213
- """
1214
- if not isinstance(actions, list):
1215
- actions = [actions]
1216
-
1217
- if not filters:
1218
- filters = dict()
1219
-
1220
- trigger_dict = {
1221
- "name": name,
1222
- "type": type_t,
1223
- "spec": {
1224
- "filter": filters,
1225
- "executionMode": execution_mode,
1226
- "resource": "Item",
1227
- "actions": actions,
1228
- "input": {},
1229
- "operation": {
1230
- "type": "function",
1231
- "functionName": function
1232
- }
1233
- }
1234
- }
1235
- return trigger_dict
1236
-
1237
- @staticmethod
1238
- def _service_json_generator(package_catalog, service_name):
1239
- """
1240
- :param package_catalog:
1241
- :param service_name:
1242
- """
1243
- triggers = list()
1244
- with open(assets.paths.ASSETS_PACKAGE_FILEPATH, 'r') as f:
1245
- service_json = json.load(f)["services"][0]
1246
- if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1247
- triggers = service_json['triggers']
1248
- elif 'triggers' in package_catalog:
1249
- trigger_a = Packages.build_trigger_dict(name='first_trigger',
1250
- filters=dict(),
1251
- actions=['Created'],
1252
- function='first_method',
1253
- execution_mode=entities.TriggerExecutionMode.ONCE)
1254
- trigger_b = Packages.build_trigger_dict(name='second_trigger',
1255
- filters=dict(),
1256
- actions=['Created'],
1257
- function='second_method',
1258
- execution_mode=entities.TriggerExecutionMode.ONCE)
1259
- if 'item' in package_catalog:
1260
- trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Item'
1261
- if 'dataset' in package_catalog:
1262
- trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Dataset'
1263
- if 'annotation' in package_catalog:
1264
- trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Annotation'
1265
- triggers += [trigger_a, trigger_b]
1266
- elif 'trigger' in package_catalog:
1267
- trigger = Packages.build_trigger_dict(name='triggername',
1268
- filters=dict(),
1269
- actions=[],
1270
- function=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1271
- execution_mode=entities.TriggerExecutionMode.ONCE)
1272
- if 'item' in package_catalog:
1273
- trigger['spec']['resource'] = 'Item'
1274
- if 'dataset' in package_catalog:
1275
- trigger['spec']['resource'] = 'Dataset'
1276
- if 'annotation' in package_catalog:
1277
- trigger['spec']['resource'] = 'Annotation'
1278
- triggers += [trigger]
1279
-
1280
- if service_name is not None:
1281
- service_json['name'] = service_name
1282
- if 'multi_module' in package_catalog:
1283
- service_json['moduleName'] = 'first_module'
1284
-
1285
- service_json['triggers'] = triggers
1286
-
1287
- return service_json
1288
-
1289
- @staticmethod
1290
- def generate(name=None,
1291
- src_path: str = None,
1292
- service_name: str = None,
1293
- package_type=PackageCatalog.DEFAULT_PACKAGE_TYPE):
1294
- """
1295
- Generate a new package. Provide a file path to a JSON file with all the details of the package and service to generate the package.
1296
-
1297
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
1298
-
1299
- :param str name: name
1300
- :param str src_path: source file path
1301
- :param str service_name: service name
1302
- :param str package_type: package type from PackageCatalog
1303
-
1304
- **Example**:
1305
-
1306
- .. code-block:: python
1307
-
1308
- project.packages.generate(name='package_name',
1309
- src_path='src_path')
1310
-
1311
- """
1312
- # name
1313
- if name is None:
1314
- name = 'default-package'
1315
- if service_name is None:
1316
- service_name = 'default-service'
1317
-
1318
- # src path
1319
- if src_path is None:
1320
- src_path = os.getcwd()
1321
-
1322
- package_asset = Packages._package_json_generator(package_name=name,
1323
- package_catalog=package_type)
1324
- services_asset = Packages._service_json_generator(package_catalog=package_type,
1325
- service_name=service_name)
1326
- package_asset["services"] = [services_asset]
1327
- to_generate = [
1328
- {'src': assets.paths.ASSETS_GITIGNORE_FILEPATH, 'dst': os.path.join(src_path, '.gitignore'),
1329
- 'type': 'copy'},
1330
- {'src': package_asset,
1331
- 'dst': os.path.join(src_path, assets.paths.PACKAGE_FILENAME), 'type': 'json'}
1332
- ]
1333
-
1334
- if package_type == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1335
- to_generate.append({'src': assets.paths.ASSETS_MOCK_FILEPATH,
1336
- 'dst': os.path.join(src_path, assets.paths.MOCK_FILENAME), 'type': 'copy'})
1337
- else:
1338
- module = entities.PackageModule.from_json(package_asset['modules'][0])
1339
- function_name = module.functions[0].name
1340
-
1341
- to_generate.append({'src': Packages._mock_json_generator(module=module, function_name=function_name),
1342
- 'dst': os.path.join(src_path, assets.paths.MOCK_FILENAME), 'type': 'json'})
1343
-
1344
- main_file_paths = Packages._entry_point_generator(package_catalog=package_type)
1345
- if len(main_file_paths) == 1:
1346
- to_generate.append({'src': main_file_paths[0],
1347
- 'dst': os.path.join(src_path, assets.paths.MAIN_FILENAME),
1348
- 'type': 'copy'})
1349
- else:
1350
- to_generate += [{'src': main_file_paths[0],
1351
- 'dst': os.path.join(src_path, assets.paths.MODULE_A_FILENAME),
1352
- 'type': 'copy'},
1353
- {'src': main_file_paths[1],
1354
- 'dst': os.path.join(src_path, assets.paths.MODULE_B_FILENAME),
1355
- 'type': 'copy'}
1356
- ]
1357
-
1358
- for job in to_generate:
1359
- if not os.path.isfile(job['dst']):
1360
- if job['type'] == 'copy':
1361
- copyfile(job['src'], job['dst'])
1362
- elif job['type'] == 'json':
1363
- with open(job['dst'], 'w') as f:
1364
- json.dump(job['src'], f, indent=2)
1365
-
1366
- logger.info('Successfully generated package')
1367
-
1368
- def build(self, package: entities.Package, module_name=None, init_inputs=None, local_path=None, from_local=None):
1369
- """
1370
- Instantiate a module from the package code. Returns a loaded instance of the runner class
1371
-
1372
- :param package: Package entity
1373
- :param module_name: Name of the module to build the runner class
1374
- :param str init_inputs: dictionary of the class init variables (if exists). will be used to init the module class
1375
- :param str local_path: local path of the package (if from_local=False - codebase will be downloaded)
1376
- :param bool from_local: bool. if true - codebase will not be downloaded (only use local files)
1377
- :return: dl.BaseServiceRunner
1378
- """
1379
- if module_name is None:
1380
- module_name = entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME
1381
- if init_inputs is None:
1382
- init_inputs = dict()
1383
- if package.codebase is None:
1384
- raise exceptions.PlatformException(error='2001',
1385
- message="Package {!r} has no codebase. unable to build a module".format(
1386
- package.name))
1387
-
1388
- if from_local is None:
1389
- from_local = package.codebase.is_local
1390
-
1391
- if not from_local:
1392
- # Not local => download codebase
1393
- if package.codebase.is_local:
1394
- raise RuntimeError("using local codebase: {}. Cannot use from_local=False".
1395
- format(package.codebase.to_json()))
1396
- local_path = self.pull(package=package, local_path=local_path)
1397
-
1398
- sys.path.append(local_path) # TODO: is it the best way to use the imports?
1399
- # load module from path
1400
- module = [module for module in package.modules if module.name == module_name]
1401
- if len(module) != 1:
1402
- raise ValueError('Cannot find module from package. trying to load: {!r}, available modules: {}'.format(
1403
- module_name,
1404
- [module.name for module in package.modules]
1405
- ))
1406
- module = module[0]
1407
- entry_point = os.path.join(local_path, module.entry_point)
1408
- class_name = module.class_name
1409
- if not os.path.isfile(entry_point):
1410
- raise ValueError('Module entry point file is missing: {}'.format(entry_point))
1411
- spec = importlib.util.spec_from_file_location(class_name, entry_point)
1412
- if spec is None:
1413
- raise ValueError('Cant load class ModelAdapter from entry point: {}'.format(entry_point))
1414
- package_module = importlib.util.module_from_spec(spec)
1415
- spec.loader.exec_module(package_module)
1416
- package_module_cls = getattr(package_module, class_name)
1417
- package_module = package_module_cls(**init_inputs)
1418
- logger.info('Model adapter loaded successfully!')
1419
- return package_module
1420
-
1421
- @staticmethod
1422
- def _mock_json_generator(module: entities.PackageModule, function_name):
1423
- """
1424
- :param module:
1425
- :param function_name:
1426
- """
1427
- _json = dict(function_name=function_name, module_name=module.name)
1428
- funcs = [func for func in module.functions if func.name == function_name]
1429
- if len(funcs) == 1:
1430
- func = funcs[0]
1431
- else:
1432
- raise exceptions.PlatformException('400', 'Other than 1 functions by the name of: {}'.format(function_name))
1433
- _json['config'] = {inpt.name: entities.Package._mockify_input(input_type=inpt.type)
1434
- for inpt in module.init_inputs}
1435
- _json['inputs'] = [{'name': inpt.name, 'value': entities.Package._mockify_input(input_type=inpt.type)}
1436
- for inpt in func.inputs]
1437
- return _json
1438
-
1439
- @staticmethod
1440
- def _entry_point_generator(package_catalog):
1441
- """
1442
- :param package_catalog:
1443
- """
1444
- if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1445
- paths_to_service_runner = assets.paths.ASSETS_MAIN_FILEPATH
1446
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ITEM, PackageCatalog.SINGLE_FUNCTION_ITEM_WITH_TRIGGER]:
1447
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH
1448
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ANNOTATION,
1449
- PackageCatalog.SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER]:
1450
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_ANNOTATION_SR_PATH
1451
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_DATASET,
1452
- PackageCatalog.SINGLE_FUNCTION_DATASET_WITH_TRIGGER]:
1453
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_DATASET_SR_PATH
1454
- elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_NO_INPUT]:
1455
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_SR_PATH
1456
- elif package_catalog == PackageCatalog.SINGLE_FUNCTION_JSON:
1457
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_JSON_SR_PATH
1458
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ITEM, PackageCatalog.MULTI_FUNCTION_ITEM_WITH_TRIGGERS]:
1459
- paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_ITEM_SR_PATH
1460
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ANNOTATION,
1461
- PackageCatalog.MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS]:
1462
- paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_ANNOTATION_SR_PATH
1463
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_DATASET,
1464
- PackageCatalog.MULTI_FUNCTION_DATASET_WITH_TRIGGERS]:
1465
- paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_DATASET_SR_PATH
1466
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_JSON]:
1467
- paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_JSON_SR_PATH
1468
- elif package_catalog in [PackageCatalog.MULTI_FUNCTION_NO_INPUT]:
1469
- paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_SR_PATH
1470
- elif package_catalog in [PackageCatalog.MULTI_MODULE, PackageCatalog.MULTI_MODULE_WITH_TRIGGER]:
1471
- paths_to_service_runner = [assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH,
1472
- assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH]
1473
- elif package_catalog == PackageCatalog.SINGLE_FUNCTION_MULTI_INPUT:
1474
- paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_MULTI_INPUT_SR_PATH
1475
- elif package_catalog == PackageCatalog.CONVERTER_FUNCTION:
1476
- paths_to_service_runner = assets.service_runner_paths.CONVERTER_SR_PATH
1477
- else:
1478
- raise exceptions.PlatformException('404', 'Unknown package catalog type: {}'.format(package_catalog))
1479
-
1480
- if not isinstance(paths_to_service_runner, list):
1481
- paths_to_service_runner = [paths_to_service_runner]
1482
-
1483
- return paths_to_service_runner
1484
-
1485
- @staticmethod
1486
- def is_multithread(inputs):
1487
- is_multi = False
1488
- if len(inputs) > 0 and isinstance(inputs[0], list):
1489
- is_multi = True
1490
-
1491
- if is_multi:
1492
- for single_input in inputs:
1493
- if not isinstance(single_input, list):
1494
- raise exceptions.PlatformException('400', 'mock.json inputs can be either list of dictionaries '
1495
- 'or list of lists')
1496
-
1497
- return is_multi
1498
-
1499
- def test_local_package(self,
1500
- cwd: str = None,
1501
- concurrency: int = None,
1502
- package: entities.Package = None,
1503
- module_name: str = entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
1504
- function_name: str = entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1505
- class_name: str = entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
1506
- entry_point: str = entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
1507
- mock_file_path: str = None):
1508
- """
1509
- Test local package in local environment.
1510
-
1511
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
1512
-
1513
- :param str cwd: path to the file
1514
- :param int concurrency: the concurrency of the test
1515
- :param dtlpy.entities.package.Package package: entities.package
1516
- :param str module_name: module name
1517
- :param str function_name: function name
1518
- :param str class_name: class name
1519
- :param str entry_point: the file to run like main.py
1520
- :param str mock_file_path: the mock file that have the inputs
1521
- :return: list created by the function that tested the output
1522
- :rtype: list
1523
-
1524
- **Example**:
1525
-
1526
- .. code-block:: python
1527
-
1528
- project.packages.test_local_package(cwd='path_to_package',
1529
- package='package_entity',
1530
- function_name='run')
1531
- """
1532
- if cwd is None:
1533
- cwd = os.getcwd()
1534
- if mock_file_path is None:
1535
- mock_file_path = assets.paths.MOCK_FILENAME
1536
- with open(os.path.join(cwd, mock_file_path), 'r') as f:
1537
- mock_json = json.load(f)
1538
- is_multithread = self.is_multithread(inputs=mock_json['inputs'])
1539
-
1540
- local_runner = LocalServiceRunner(self._client_api,
1541
- packages=self,
1542
- cwd=cwd,
1543
- package=package,
1544
- multithreading=is_multithread,
1545
- concurrency=concurrency,
1546
- module_name=module_name,
1547
- function_name=function_name,
1548
- class_name=class_name,
1549
- entry_point=entry_point,
1550
- mock_file_path=mock_file_path)
1551
-
1552
- if self._project is None:
1553
- try:
1554
- project = repositories.Projects(client_api=self._client_api).get()
1555
- except Exception:
1556
- project = None
1557
- else:
1558
- project = self.project
1559
-
1560
- return local_runner.run_local_project(project=project)
1561
-
1562
- def __get_from_cache(self) -> entities.Package:
1563
- package = self._client_api.state_io.get('package')
1564
- if package is not None:
1565
- package = entities.Package.from_json(_json=package, client_api=self._client_api, project=self._project)
1566
- return package
1567
-
1568
- def checkout(self,
1569
- package: entities.Package = None,
1570
- package_id: str = None,
1571
- package_name: str = None):
1572
- """
1573
- Checkout (switch) to a package.
1574
-
1575
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
1576
-
1577
- :param dtlpy.entities.package.Package package: package entity
1578
- :param str package_id: package id
1579
- :param str package_name: package name
1580
-
1581
- **Example**:
1582
-
1583
- .. code-block:: python
1584
-
1585
- project.packages.checkout(package='package_entity')
1586
-
1587
- """
1588
- if package is None:
1589
- package = self.get(package_id=package_id, package_name=package_name)
1590
- self._client_api.state_io.put('package', package.to_json())
1591
- logger.info("Checked out to package {}".format(package.name))
1592
-
1593
- @staticmethod
1594
- def check_cls_arguments(cls, missing, function_name, function_inputs):
1595
- """
1596
- Check class arguments. This method checks that the package function is correct.
1597
-
1598
- **Prerequisites**: You must be in the role of an *owner* or *developer*.
1599
-
1600
- :param cls: packages class
1601
- :param list missing: list of the missing params
1602
- :param str function_name: name of function
1603
- :param list function_inputs: list of function inputs
1604
- """
1605
- # input to function and inputs definitions match
1606
- func = getattr(cls, function_name)
1607
- func_inspect = inspect.getfullargspec(func)
1608
- if not isinstance(function_inputs, list):
1609
- function_inputs = [function_inputs]
1610
- defined_input_names = [inp['name'] if isinstance(inp, dict) else inp.name for inp in function_inputs]
1611
-
1612
- args_with_values = dict()
1613
- # go over the defaults and inputs from end to start to map between them
1614
- if func_inspect.defaults is not None:
1615
- for i_value in range(-1, -len(func_inspect.defaults) - 1, -1):
1616
- args_with_values[func_inspect.args[i_value]] = func_inspect.defaults[i_value]
1617
-
1618
- for input_name in func_inspect.args:
1619
- if input_name in ['self', 'progress', 'context']:
1620
- continue
1621
- if input_name not in defined_input_names:
1622
- logger.warning('missing input name: "{}" in function definition: "{}"'.format(input_name,
1623
- function_name))
1624
- if input_name not in args_with_values:
1625
- missing.append('missing input name: "{}" in function definition: "{}"'.format(input_name,
1626
- function_name))
1627
-
1628
- @staticmethod
1629
- def _sanity_before_push(src_path, modules):
1630
- """
1631
- :param src_path:
1632
- :param modules:
1633
- """
1634
- if modules is None:
1635
- modules = [DEFAULT_PACKAGE_MODULE]
1636
-
1637
- if not isinstance(modules, list):
1638
- modules = [modules]
1639
-
1640
- if not isinstance(modules[0], entities.PackageModule):
1641
- modules = [entities.PackageModule.from_json(_json=module) for module in modules]
1642
-
1643
- missing = list()
1644
- for module in modules:
1645
- entry_point = module.entry_point
1646
- class_name = module.class_name
1647
-
1648
- check_init = True
1649
- functions = module.functions
1650
- # check in inputs is a list
1651
- if not isinstance(functions, list):
1652
- functions = [functions]
1653
-
1654
- for function in functions:
1655
- # entry point exists
1656
- entry_point_filepath = os.path.join(src_path, entry_point)
1657
- if not os.path.isfile(entry_point_filepath):
1658
- missing.append('missing entry point file: {}'.format(entry_point_filepath))
1659
- continue
1660
-
1661
- # class name in entry point
1662
- try:
1663
- spec = importlib.util.spec_from_file_location(class_name, entry_point_filepath)
1664
- cls_def = importlib.util.module_from_spec(spec)
1665
- spec.loader.exec_module(cls_def)
1666
- except ImportError as e:
1667
- logger.warning(
1668
- 'Cannot run sanity check to find conflicts between package module and source code'
1669
- 'because some packages in your source code are not installed in your local environment'.format(
1670
- e.__str__())
1671
- )
1672
- return modules
1673
-
1674
- if not hasattr(cls_def, class_name):
1675
- missing.append('missing class: "{}" from file: "{}"'.format(class_name, entry_point_filepath))
1676
- continue
1677
-
1678
- # function in class
1679
- cls = getattr(cls_def, class_name)
1680
- if not hasattr(cls, function.name):
1681
- missing.append(
1682
- 'missing function: "{}" from class: "{}"'.format(function.name, entry_point_filepath))
1683
- continue
1684
-
1685
- if check_init:
1686
- check_init = False
1687
- # input to __init__ and inputs definitions match
1688
- Packages.check_cls_arguments(cls=cls,
1689
- missing=missing,
1690
- function_name="__init__",
1691
- function_inputs=module.init_inputs)
1692
-
1693
- Packages.check_cls_arguments(cls=cls,
1694
- missing=missing,
1695
- function_name=function.name,
1696
- function_inputs=function.inputs)
1697
-
1698
- if len(missing) != 0:
1699
- raise ValueError('There are conflicts between modules and source code:'
1700
- '\n{}\n{}'.format(missing,
1701
- 'Please fix conflicts or use flag ignore_sanity_check'))
1702
-
1703
- return modules
1704
-
1705
-
1706
- class LocalServiceRunner:
1707
- """
1708
- Service Runner Class
1709
- """
1710
-
1711
- def __init__(self,
1712
- client_api: ApiClient,
1713
- packages,
1714
- cwd=None,
1715
- multithreading=False,
1716
- concurrency=10,
1717
- package: entities.Package = None,
1718
- module_name=entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
1719
- function_name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1720
- class_name=entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
1721
- entry_point=entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
1722
- mock_file_path=None):
1723
- if cwd is None:
1724
- self.cwd = os.getcwd()
1725
- else:
1726
- self.cwd = cwd
1727
-
1728
- self._client_api = client_api
1729
- self._packages = packages
1730
- self.package_io = PackageIO(cwd=self.cwd)
1731
- self.multithreading = multithreading
1732
- self.concurrency = concurrency
1733
- self._module_name = module_name
1734
- self._function_name = function_name
1735
- self._entry_point = entry_point
1736
- self._class_name = class_name
1737
- self.package = package
1738
-
1739
- if mock_file_path is None:
1740
- mock_file_path = 'mock.json'
1741
- with open(os.path.join(self.cwd, mock_file_path), 'r') as f:
1742
- self.mock_json = json.load(f)
1743
-
1744
- @property
1745
- def function_name(self):
1746
- return self.mock_json.get('function_name', self._function_name)
1747
-
1748
- @property
1749
- def module_name(self):
1750
- return self.mock_json.get('module_name', self._module_name)
1751
-
1752
- @property
1753
- def class_name(self):
1754
- return self.mock_json.get('class_name', self._class_name)
1755
-
1756
- @property
1757
- def entry_point(self):
1758
- return self.mock_json.get('entry_point', self._entry_point)
1759
-
1760
- def get_mainpy_run_service(self):
1761
- """
1762
- Get mainpy run service
1763
-
1764
- :return:
1765
- """
1766
- entry_point = os.path.join(self.cwd, self.entry_point)
1767
- spec = importlib.util.spec_from_file_location(self.class_name, entry_point)
1768
- cls_def = importlib.util.module_from_spec(spec)
1769
- spec.loader.exec_module(cls_def)
1770
- service_runner = getattr(cls_def, self.class_name)
1771
-
1772
- if 'init_params' in self.mock_json:
1773
- kwargs = self.mock_json.get('init_params')
1774
- elif 'config' in self.mock_json:
1775
- kwargs = self.mock_json.get('config')
1776
- else:
1777
- kwargs = dict()
1778
-
1779
- return service_runner(**kwargs)
1780
-
1781
- def run_local_project(self, project=None):
1782
- """
1783
- Run local project
1784
-
1785
- :param project: project entity
1786
- """
1787
- package_runner = self.get_mainpy_run_service()
1788
-
1789
- modules = self.package_io.get('modules') if self.package is None else [
1790
- m.to_json() for m in self.package.modules
1791
- ]
1792
-
1793
- if isinstance(modules, list) and len(modules) > 0:
1794
- try:
1795
- module = [module for module in modules if module['name'] == self.module_name][0]
1796
- except Exception:
1797
- raise exceptions.PlatformException(
1798
- '400', 'Module {} does not exist in package modules'.format(self.module_name)
1799
- )
1800
- else:
1801
- module = DEFAULT_PACKAGE_MODULE.to_json()
1802
-
1803
- if isinstance(module['functions'], list) and len(module['functions']) > 0:
1804
- try:
1805
- func = [func for func in module['functions'] if func['name'] == self.function_name][0]
1806
- except Exception:
1807
- raise exceptions.PlatformException(
1808
- '400', 'function {} does not exist in package module'.format(self.function_name)
1809
- )
1810
- else:
1811
- func = entities.PackageFunction(None, list(), list(), None).to_json()
1812
- package_inputs = func['input']
1813
- current_level = logging.root.level
1814
- logging.root.setLevel('INFO')
1815
- if not self.multithreading:
1816
- kwargs = dict()
1817
- progress = utilities.Progress()
1818
-
1819
- func = getattr(package_runner, self.function_name)
1820
- params = list(inspect.signature(func).parameters)
1821
- if "progress" in params:
1822
- kwargs['progress'] = progress
1823
-
1824
- for package_input in package_inputs:
1825
- kwargs[package_input['name']] = self.get_field(field_name=package_input['name'],
1826
- field_type=package_input['type'],
1827
- project=project, mock_json=self.mock_json)
1828
-
1829
- results = getattr(package_runner, self.function_name)(**kwargs)
1830
- else:
1831
- pool = ThreadPoolExecutor(max_workers=self.concurrency)
1832
- inputs = self.mock_json['inputs']
1833
- results = list()
1834
- jobs = list()
1835
- for single_input in inputs:
1836
- kwargs = dict()
1837
- progress = utilities.Progress()
1838
- kwargs['progress'] = progress
1839
- for package_input in package_inputs:
1840
- kwargs[package_input['name']] = self.get_field(field_name=package_input['name'],
1841
- field_type=package_input['type'],
1842
- project=project,
1843
- mock_json=self.mock_json,
1844
- mock_inputs=single_input)
1845
- jobs.append(
1846
- pool.submit(
1847
- getattr(package_runner, self.function_name),
1848
- **kwargs
1849
- )
1850
- )
1851
- for job in jobs:
1852
- results.append(job.result())
1853
-
1854
- logging.root.level = current_level
1855
- return results
1856
-
1857
- @staticmethod
1858
- def _is_entity(output_type: str):
1859
- # output_type is a entity class starts with capital
1860
- return hasattr(entities, output_type)
1861
-
1862
- def _fetch_single_resource(self, value, output_type):
1863
- if isinstance(value, dict):
1864
- params = {'{}_id'.format(output_type.lower()): value['{}_id'.format(output_type.lower())]}
1865
- else:
1866
- params = {'{}_id'.format(output_type.lower()): value}
1867
- return getattr(repositories, '{}s'.format(output_type))(self._client_api).get(**params)
1868
-
1869
- def get_field(self, field_name, field_type, mock_json, project=None, mock_inputs=None):
1870
- """
1871
- Get field in mock json.
1872
-
1873
- :param field_name: field name
1874
- :param field_type: field type
1875
- :param mock_json: mock json
1876
- :param project: project
1877
- :param mock_inputs: mock inputs
1878
- :return:
1879
- """
1880
- if mock_inputs is None:
1881
- mock_inputs = mock_json['inputs']
1882
- filtered_mock_inputs = list(filter(lambda input_field: input_field['name'] == field_name, mock_inputs))
1883
-
1884
- if len(filtered_mock_inputs) == 0:
1885
- raise Exception('No entry for field {} found in mock'.format(field_name))
1886
- if len(filtered_mock_inputs) > 1:
1887
- raise Exception('Duplicate entries for field {} found in mock'.format(field_name))
1888
-
1889
- if field_type not in list(entities.PackageInputType):
1890
- raise exceptions.PlatformException('400', 'Unknown resource type for field {}'.format(field_name))
1891
-
1892
- mock_input = filtered_mock_inputs[0]
1893
- resource_id = mock_input['value']
1894
-
1895
- if field_type.endswith('[]') and self._is_entity(output_type=field_type.replace('[]', '')):
1896
- field_type = field_type.replace('[]', '')
1897
- value = resource_id if isinstance(resource_id, list) else [resource_id]
1898
- return [
1899
- self._fetch_single_resource(value=val, output_type=field_type) for val in value
1900
- ]
1901
- elif self._is_entity(output_type=field_type):
1902
- return self._fetch_single_resource(value=resource_id, output_type=field_type)
1903
- else:
1904
- return resource_id
1905
-
1906
-
1907
- class PackageIO:
1908
-
1909
- def __init__(self, cwd=None):
1910
- if cwd is None:
1911
- cwd = os.getcwd()
1912
-
1913
- self.package_file_path = os.path.join(cwd, assets.paths.PACKAGE_FILENAME)
1914
-
1915
- def read_json(self):
1916
- file_path = self.package_file_path
1917
-
1918
- with open(file_path, 'r') as fp:
1919
- cfg = json.load(fp)
1920
- return cfg
1921
-
1922
- def get(self, key):
1923
- """
1924
- :param key:
1925
- """
1926
- cfg = self.read_json()
1927
- return cfg[key]
1928
-
1929
- def put(self, key, value):
1930
- """
1931
- :param key:
1932
- :param value:
1933
- """
1934
- try:
1935
- cfg = self.read_json()
1936
- cfg[key] = value
1937
- file_path = self.package_file_path
1938
- with open(file_path, 'w') as fp:
1939
- json.dump(cfg, fp, indent=2)
1940
- except Exception:
1941
- pass
1
+ import collections.abc
2
+ import importlib.util
3
+ import inspect
4
+ import logging
5
+ import hashlib
6
+ import typing
7
+ import json
8
+ import sys
9
+ import os
10
+ import re
11
+ from copy import deepcopy
12
+ from shutil import copyfile
13
+ from concurrent.futures import ThreadPoolExecutor
14
+
15
+ from .. import entities, repositories, exceptions, utilities, miscellaneous, assets, services, _api_reference
16
+ from ..services.api_client import ApiClient
17
+
18
+ logger = logging.getLogger(name='dtlpy')
19
+
20
+ DEFAULT_PACKAGE_METHOD = entities.PackageFunction(name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
21
+ description='',
22
+ inputs=[],
23
+ outputs=[])
24
+ DEFAULT_PACKAGE_MODULE = entities.PackageModule(init_inputs=list(),
25
+ entry_point=entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
26
+ class_name=entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
27
+ name=entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
28
+ functions=[DEFAULT_PACKAGE_METHOD])
29
+
30
+
31
+ class PackageCatalog:
32
+ DEFAULT_PACKAGE_TYPE = 'default_package_type'
33
+ MULTI_MODULE = 'multi_module'
34
+ MULTI_MODULE_WITH_TRIGGER = 'multi_module_with_trigger'
35
+ SINGLE_FUNCTION_ITEM = 'single_function_item'
36
+ SINGLE_FUNCTION_JSON = 'single_function_json'
37
+ SINGLE_FUNCTION_DATASET = 'single_function_dataset'
38
+ SINGLE_FUNCTION_ANNOTATION = 'single_function_annotation'
39
+ SINGLE_FUNCTION_NO_INPUT = 'single_function_no_input'
40
+ SINGLE_FUNCTION_MULTI_INPUT = 'single_function_multi_input'
41
+ MULTI_FUNCTION_ITEM = 'multi_function_item'
42
+ MULTI_FUNCTION_DATASET = 'multi_function_dataset'
43
+ MULTI_FUNCTION_ANNOTATION = 'multi_function_annotation'
44
+ MULTI_FUNCTION_NO_INPUT = 'multi_function_no_input'
45
+ MULTI_FUNCTION_JSON = 'multi_function_json'
46
+ SINGLE_FUNCTION_ITEM_WITH_TRIGGER = 'single_function_item_with_trigger'
47
+ SINGLE_FUNCTION_DATASET_WITH_TRIGGER = 'single_function_dataset_with_trigger'
48
+ SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER = 'single_function_annotation_with_trigger'
49
+ MULTI_FUNCTION_ITEM_WITH_TRIGGERS = 'multi_function_item_with_triggers'
50
+ MULTI_FUNCTION_DATASET_WITH_TRIGGERS = 'multi_function_dataset_with_triggers'
51
+ MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS = 'multi_function_annotation_with_triggers'
52
+ CONVERTER_FUNCTION = 'converter'
53
+
54
+
55
+ class Packages:
56
+ """
57
+ Packages Repository
58
+
59
+ The Packages class allows users to manage packages (code used for running in Dataloop's FaaS) and their properties.
60
+ Read more about `Packages <https://developers.dataloop.ai/tutorials/faas/introduction/chapter/>`_.
61
+ """
62
+
63
+ def __init__(self, client_api: ApiClient, project: entities.Project = None):
64
+ self._client_api = client_api
65
+ self._project = project
66
+ self.package_io = PackageIO()
67
+
68
+ ############
69
+ # entities #
70
+ ############
71
+ @property
72
+ def project(self) -> entities.Project:
73
+ if self._project is None:
74
+ try:
75
+ self._project = repositories.Projects(client_api=self._client_api).get()
76
+ except exceptions.NotFound:
77
+ raise exceptions.PlatformException(
78
+ error='2001',
79
+ message='Missing "project". need to set a Project entity or use project.packages repository')
80
+ return self._project
81
+
82
+ @project.setter
83
+ def project(self, project: entities.Project):
84
+ if not isinstance(project, entities.Project):
85
+ raise ValueError('Must input a valid Project entity')
86
+ self._project = project
87
+
88
+ ###########
89
+ # methods #
90
+ ###########
91
+ @property
92
+ def platform_url(self):
93
+ return self._client_api._get_resource_url("projects/{}/packages".format(self.project.id))
94
+
95
+ def open_in_web(self,
96
+ package: entities.Package = None,
97
+ package_id: str = None,
98
+ package_name: str = None):
99
+ """
100
+ Open the package in the web platform.
101
+
102
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
103
+
104
+ :param dtlpy.entities.package.Package package: package entity
105
+ :param str package_id: package id
106
+ :param str package_name: package name
107
+
108
+ **Example**:
109
+
110
+ .. code-block:: python
111
+
112
+ project.packages.open_in_web(package_id='package_id')
113
+ """
114
+ if package_name is not None:
115
+ package = self.get(package_name=package_name)
116
+ if package is not None:
117
+ package.open_in_web()
118
+ elif package_id is not None:
119
+ self._client_api._open_in_web(url=self.platform_url + '/' + str(package_id) + '/main')
120
+ else:
121
+ self._client_api._open_in_web(url=self.platform_url)
122
+
123
+ @_api_reference.add(path='/packages/{id}/revisions', method='get')
124
+ def revisions(self, package: entities.Package = None, package_id: str = None):
125
+ """
126
+ Get the package revision history.
127
+
128
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
129
+
130
+ :param dtlpy.entities.package.Package package: package entity
131
+ :param str package_id: package id
132
+
133
+ **Example**:
134
+
135
+ .. code-block:: python
136
+
137
+ project.packages.revisions(package='package_entity')
138
+ """
139
+ if package is None and package_id is None:
140
+ raise exceptions.PlatformException(
141
+ error='400',
142
+ message='must provide an identifier in inputs: "package" or "package_id"')
143
+ if package is not None:
144
+ package_id = package.id
145
+
146
+ success, response = self._client_api.gen_request(
147
+ req_type="get",
148
+ path="/packages/{}/revisions".format(package_id))
149
+ if not success:
150
+ raise exceptions.PlatformException(response)
151
+ return response.json()
152
+
153
+ @_api_reference.add(path='/packages/{id}', method='get')
154
+ def get(self,
155
+ package_name: str = None,
156
+ package_id: str = None,
157
+ checkout: bool = False,
158
+ fetch=None,
159
+ log_error=True) -> entities.Package:
160
+ """
161
+ Get Package object to use in your code.
162
+
163
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
164
+
165
+ :param str package_id: package id
166
+ :param str package_name: package name
167
+ :param bool checkout: set the package as a default package object (cookies)
168
+ :param fetch: optional - fetch entity from platform, default taken from cookie
169
+ :param bool log_error: log error if package not found
170
+ :return: Package object
171
+ :rtype: dtlpy.entities.package.Package
172
+
173
+ **Example**:
174
+
175
+ .. code-block:: python
176
+
177
+ project.packages.get(package_id='package_id')
178
+ """
179
+ if fetch is None:
180
+ fetch = self._client_api.fetch_entities
181
+
182
+ if package_name is None and package_id is None:
183
+ package = self.__get_from_cache()
184
+ if package is None:
185
+ raise exceptions.PlatformException(
186
+ error='400',
187
+ message='No checked-out Package was found, must checkout or provide an identifier in inputs')
188
+ elif fetch:
189
+ if package_id is not None:
190
+ success, response = self._client_api.gen_request(
191
+ req_type="get",
192
+ path="/packages/{}".format(package_id),
193
+ log_error=log_error)
194
+ if not success:
195
+ raise exceptions.PlatformException(response)
196
+ package = entities.Package.from_json(client_api=self._client_api,
197
+ _json=response.json(),
198
+ project=self._project)
199
+ # verify input package name is same as the given id
200
+ if package_name is not None and package.name != package_name:
201
+ logger.warning(
202
+ "Mismatch found in packages.get: package_name is different then package.name:"
203
+ " {!r} != {!r}".format(
204
+ package_name,
205
+ package.name))
206
+ elif package_name is not None:
207
+ filters = entities.Filters(field='name', values=package_name, resource=entities.FiltersResource.PACKAGE,
208
+ use_defaults=False)
209
+ if self._project is not None:
210
+ filters.add(field='projectId', values=self._project.id)
211
+ packages = self.list(filters=filters)
212
+ if packages.items_count == 0:
213
+ raise exceptions.PlatformException(
214
+ error='404',
215
+ message='Package not found. Name: {}'.format(package_name))
216
+ elif packages.items_count > 1:
217
+ raise exceptions.PlatformException(
218
+ error='400',
219
+ message='More than one package found by the name of: {} '
220
+ 'Please get package from a project entity'.format(package_name))
221
+ package = packages.items[0]
222
+ else:
223
+ raise exceptions.PlatformException(
224
+ error='400',
225
+ message='No checked-out Package was found, must checkout or provide an identifier in inputs')
226
+ else:
227
+ package = entities.Package.from_json(_json={'id': package_id,
228
+ 'name': package_name},
229
+ client_api=self._client_api,
230
+ project=self._project,
231
+ is_fetched=False)
232
+
233
+ if checkout:
234
+ self.checkout(package=package)
235
+ return package
236
+
237
+ def _build_entities_from_response(self, response_items) -> miscellaneous.List[entities.Package]:
238
+ pool = self._client_api.thread_pools(pool_name='entity.create')
239
+ jobs = [None for _ in range(len(response_items))]
240
+ # return triggers list
241
+ for i_package, package in enumerate(response_items):
242
+ jobs[i_package] = pool.submit(entities.Package._protected_from_json,
243
+ **{'client_api': self._client_api,
244
+ '_json': package,
245
+ 'project': self._project})
246
+
247
+ # get all results
248
+ results = [j.result() for j in jobs]
249
+ # log errors
250
+ _ = [logger.warning(r[1]) for r in results if r[0] is False]
251
+ # return good jobs
252
+ packages = miscellaneous.List([r[1] for r in results if r[0] is True])
253
+ return packages
254
+
255
+ def _list(self, filters: entities.Filters):
256
+ url = '/query/faas'
257
+
258
+ # request
259
+ success, response = self._client_api.gen_request(req_type='post',
260
+ path=url,
261
+ json_req=filters.prepare())
262
+ if not success:
263
+ raise exceptions.PlatformException(response)
264
+ return response.json()
265
+
266
+ @_api_reference.add(path='/query/faas', method='post')
267
+ def list(self, filters: entities.Filters = None, project_id: str = None) -> entities.PagedEntities:
268
+ """
269
+ List project packages.
270
+
271
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
272
+
273
+ :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
274
+ :param str project_id: project id
275
+ :return: Paged entity
276
+ :rtype: dtlpy.entities.paged_entities.PagedEntities
277
+
278
+ **Example**:
279
+
280
+ .. code-block:: python
281
+
282
+ project.packages.list()
283
+ """
284
+ if filters is None:
285
+ filters = entities.Filters(resource=entities.FiltersResource.PACKAGE)
286
+ # assert type filters
287
+ elif not isinstance(filters, entities.Filters):
288
+ raise exceptions.PlatformException(error='400',
289
+ message='Unknown filters type: {!r}'.format(type(filters)))
290
+ if filters.resource != entities.FiltersResource.PACKAGE:
291
+ raise exceptions.PlatformException(
292
+ error='400',
293
+ message='Filters resource must to be FiltersResource.PACKAGE. Got: {!r}'.format(filters.resource))
294
+
295
+ if project_id is None and self._project is not None:
296
+ project_id = self._project.id
297
+
298
+ if project_id is not None:
299
+ filters.add(field='projectId', values=project_id)
300
+
301
+ paged = entities.PagedEntities(items_repository=self,
302
+ filters=filters,
303
+ page_offset=filters.page,
304
+ page_size=filters.page_size,
305
+ project_id=project_id,
306
+ client_api=self._client_api)
307
+ paged.get_page()
308
+ return paged
309
+
310
+ def pull(self, package: entities.Package, version=None, local_path=None, project_id=None):
311
+ """
312
+ Pull (download) the package to a local path.
313
+
314
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
315
+
316
+ :param dtlpy.entities.package.Package package: package entity
317
+ :param str version: the package version to pull
318
+ :param local_path: the path of where to save the package
319
+ :param project_id: the project id that include the package
320
+ :return: local path where the package pull
321
+ :rtype: str
322
+
323
+ **Example**:
324
+
325
+ .. code-block:: python
326
+
327
+ project.packages.pull(package='package_entity', local_path='local_path')
328
+ """
329
+ if isinstance(version, int):
330
+ logger.warning('Deprecation Warning - Package/service versions have been refactored'
331
+ 'The version you provided has type: int, it will be converted to: 1.0.{}'
332
+ 'Next time use a 3-level semver for package/service versions'.format(version))
333
+
334
+ dir_version = version
335
+ if version is None:
336
+ dir_version = package.version
337
+
338
+ if local_path is None:
339
+ local_path = os.path.join(
340
+ services.service_defaults.DATALOOP_PATH,
341
+ "packages",
342
+ "{}-{}".format(package.name, package.id),
343
+ str(dir_version))
344
+
345
+ if version is None or version == package.version:
346
+ package_revision = package
347
+ else:
348
+ versions = [revision for revision in package.revisions if revision['version'] == version]
349
+ if len(versions) <= 0:
350
+ raise exceptions.PlatformException('404', 'Version not found: version={}'.format(version))
351
+ elif len(versions) > 1:
352
+ raise exceptions.PlatformException('404', 'More than one version found: version={}'.format(version))
353
+ package_revision = entities.Package.from_json(
354
+ _json=versions[0],
355
+ project=package.project,
356
+ client_api=package._client_api
357
+ )
358
+ if package_revision.codebase.type == entities.PackageCodebaseType.ITEM:
359
+ local_path = package_revision.codebases.unpack(codebase_id=package_revision.codebase.item_id,
360
+ local_path=local_path)
361
+ elif package_revision.codebase.type == entities.PackageCodebaseType.GIT:
362
+ local_path = package_revision.codebases.unpack(codebase=package_revision.codebase,
363
+ local_path=local_path)
364
+ else:
365
+ raise Exception(
366
+ 'Pull can only be performed on packages with Item or Git codebase. Codebase type: {!r}'.format(
367
+ package_revision.codebase.type))
368
+
369
+ return local_path
370
+
371
+ def _name_validation(self, name: str):
372
+ url = '/piper-misc/naming/packages/{}'.format(name)
373
+
374
+ # request
375
+ success, response = self._client_api.gen_request(req_type='get',
376
+ path=url)
377
+ if not success:
378
+ raise exceptions.PlatformException(response)
379
+
380
+ @staticmethod
381
+ def _validate_slots(slots, modules):
382
+ for slot in slots:
383
+ matched_module = [module for module in modules if slot.module_name == module.name]
384
+ if len(matched_module) != 1:
385
+ raise ValueError('Module {!r} in slots is not defined in modules.'.format(slot.module_name))
386
+ matched_func = [func for func in matched_module[0].functions if slot.function_name == func.name]
387
+ if len(matched_func) != 1:
388
+ raise ValueError('Function {!r} in slots is not defined in module {!r}.'.format(slot.function_name,
389
+ slot.module_name))
390
+
391
+ def build_requirements(self, filepath) -> list:
392
+ """
393
+ Build a requirement list (list of packages your code requires to run) from a file path. **The file listing the requirements MUST BE a txt file**.
394
+
395
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
396
+
397
+ :param filepath: path of the requirements file
398
+ :return: a list of dl.PackageRequirement
399
+ :rtype: list
400
+ """
401
+ requirements_list = []
402
+ if not os.path.exists(filepath):
403
+ raise exceptions.PlatformException('400', 'requirements file {} does not exist'.format(filepath))
404
+ with open(filepath) as requirements_txt:
405
+ for requirement in requirements_txt:
406
+ requirement = requirement.strip()
407
+ if '\n' in requirement:
408
+ requirement.replace('\n', '')
409
+ if requirement.startswith('#'):
410
+ continue
411
+ elif ' #' in requirement:
412
+ requirement = requirement[:requirement.find(' #')]
413
+ if 'install -r' in requirement:
414
+ requirements_list.extend(self.build_requirements(requirement[requirement.find('-r ') + 3:]))
415
+ continue
416
+ requirement_spit = requirement.split(',')
417
+ for req in requirement_spit:
418
+ name_version = re.split('<=|>=|==|<|>', req)
419
+ req_name = name_version[0]
420
+ req_version, op = None, None
421
+ if len(name_version) > 1:
422
+ op = re.findall('<=|>=|==|<|>', req)[0]
423
+ req_version = name_version[1]
424
+ if req_name == '':
425
+ req_name = last_req_name
426
+ last_req_name = req_name
427
+ requirements_list.append(
428
+ entities.PackageRequirement(name=req_name, version=req_version, operator=op))
429
+ return requirements_list
430
+
431
+ @_api_reference.add(path='/packages', method='post')
432
+ def push(self,
433
+ project: entities.Project = None,
434
+ project_id: str = None,
435
+ package_name: str = None,
436
+ src_path: str = None,
437
+ codebase: typing.Union[entities.GitCodebase, entities.ItemCodebase, entities.FilesystemCodebase] = None,
438
+ modules: typing.List[entities.PackageModule] = None,
439
+ is_global: bool = None,
440
+ checkout: bool = False,
441
+ revision_increment: str = None,
442
+ version: str = None,
443
+ ignore_sanity_check: bool = False,
444
+ service_update: bool = False,
445
+ service_config: dict = None,
446
+ slots: typing.List[entities.PackageSlot] = None,
447
+ requirements: typing.List[entities.PackageRequirement] = None,
448
+ package_type=None,
449
+ metadata=None
450
+ ) -> entities.Package:
451
+ """
452
+ Push your local package to the UI.
453
+
454
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
455
+
456
+ Project will be taken in the following hierarchy:
457
+ project(input) -> project_id(input) -> self.project(context) -> checked out
458
+
459
+ :param dtlpy.entities.project.Project project: optional - project entity to deploy to. default from context or checked-out
460
+ :param str project_id: optional - project id to deploy to. default from context or checked-out
461
+ :param str package_name: package name
462
+ :param str src_path: path to package codebase
463
+ :param dtlpy.entities.codebase.Codebase codebase: codebase object
464
+ :param list modules: list of modules PackageModules of the package
465
+ :param bool is_global: is package is global or local
466
+ :param bool checkout: checkout package to local dir
467
+ :param str revision_increment: optional - str - version bumping method - major/minor/patch - default = None
468
+ :param str version: semver version f the package
469
+ :param bool ignore_sanity_check: NOT RECOMMENDED - skip code sanity check before pushing
470
+ :param bool service_update: optional - bool - update the service
471
+ :param dict service_config : Service object as dict. Contains the spec of the default service to create.
472
+ :param list slots: optional - list of slots PackageSlot of the package
473
+ :param list requirements: requirements - list of package requirements
474
+ :param str package_type: default 'faas', options: 'app', 'ml
475
+ :param dict metadata: dictionary of system and user metadata
476
+
477
+ :return: Package object
478
+ :rtype: dtlpy.entities.package.Package
479
+
480
+ **Example**:
481
+
482
+ .. code-block:: python
483
+
484
+ project.packages.push(package_name='package_name',
485
+ modules=[module],
486
+ version='1.0.0',
487
+ src_path=os.getcwd()
488
+ )
489
+ """
490
+ # get project
491
+ project_to_deploy = None
492
+ if project is not None:
493
+ project_to_deploy = project
494
+ elif project_id is not None:
495
+ project_to_deploy = repositories.Projects(client_api=self._client_api).get(project_id=project_id)
496
+ elif self._project is not None:
497
+ project_to_deploy = self._project
498
+ else:
499
+ try:
500
+ project_to_deploy = repositories.Projects(client_api=self._client_api).get()
501
+ except Exception:
502
+ pass
503
+
504
+ if project_to_deploy is None:
505
+ raise exceptions.PlatformException(
506
+ error='400',
507
+ message='Missing project from "packages.push" function. '
508
+ 'Please provide project or id, use Packages from a '
509
+ 'project.packages repository or checkout a project')
510
+
511
+ # source path
512
+ if src_path is None:
513
+ if codebase is None:
514
+ src_path = os.getcwd()
515
+ logger.warning('No src_path is given, getting package information from cwd: {}'.format(src_path))
516
+
517
+ # get package json
518
+ package_from_json = dict()
519
+ path_files = os.listdir(src_path) if src_path is not None else []
520
+ if assets.paths.PACKAGE_FILENAME in path_files:
521
+ with open(os.path.join(src_path, assets.paths.PACKAGE_FILENAME), 'r') as f:
522
+ package_from_json = json.load(f)
523
+
524
+ if requirements is not None and not isinstance(requirements, list):
525
+ requirements = [requirements]
526
+
527
+ if requirements and assets.paths.REQUIREMENTS_FILENAME in path_files:
528
+ logger.warning('Have both requirements param and requirements file will overwrite the requirements file')
529
+
530
+ if not requirements and assets.paths.REQUIREMENTS_FILENAME in path_files:
531
+ req_path = os.path.join(src_path, assets.paths.REQUIREMENTS_FILENAME)
532
+ req_from_file = self.build_requirements(filepath=req_path)
533
+ requirements = req_from_file
534
+
535
+ # get name
536
+ if package_name is None:
537
+ package_name = package_from_json.get('name', 'default-package')
538
+
539
+ if modules is None and 'modules' in package_from_json:
540
+ modules = package_from_json['modules']
541
+
542
+ if slots is None and 'slots' in package_from_json:
543
+ slots = package_from_json['slots']
544
+
545
+ if ignore_sanity_check:
546
+ logger.warning(
547
+ 'Pushing a package without sanity check can cause errors when trying to deploy, '
548
+ 'trigger and execute functions.\n'
549
+ 'We highly recommend to not use the ignore_sanity_check flag')
550
+ elif codebase is None:
551
+ modules = self._sanity_before_push(src_path=src_path, modules=modules)
552
+
553
+ self._name_validation(name=package_name)
554
+
555
+ if slots is not None:
556
+ if modules is None:
557
+ raise ValueError('Cannot add slots when modules is empty.')
558
+ if not isinstance(slots[0], entities.PackageSlot):
559
+ slots = [entities.PackageSlot.from_json(_json=slot) for slot in slots]
560
+ self._validate_slots(slots, modules)
561
+
562
+ delete_codebase = False
563
+ if codebase is None:
564
+ codebase = project_to_deploy.codebases.pack(directory=src_path, name=package_name)
565
+ delete_codebase = True
566
+
567
+ try:
568
+ # check if exist
569
+ filters = entities.Filters(resource=entities.FiltersResource.PACKAGE, use_defaults=False)
570
+ filters.add(field='projectId', values=project_to_deploy.id)
571
+ filters.add(field='name', values=package_name)
572
+ packages = self.list(filters=filters)
573
+ if packages.items_count > 0:
574
+ # package exists - need to update
575
+ package = packages.items[0]
576
+
577
+ if modules is not None:
578
+ package.modules = modules
579
+
580
+ if slots is not None:
581
+ package.slots = slots
582
+
583
+ if is_global is not None:
584
+ package.is_global = is_global
585
+
586
+ if codebase is not None:
587
+ package.codebase = codebase
588
+
589
+ if version is not None:
590
+ package.version = version
591
+
592
+ if requirements is not None:
593
+ package.requirements = requirements
594
+
595
+ if service_config is not None:
596
+ package.service_config = service_config
597
+
598
+ if metadata is not None:
599
+ package.metadata = metadata
600
+
601
+ if package_type is not None:
602
+ package.package_type = package_type
603
+ package = self.update(package=package, revision_increment=revision_increment)
604
+ else:
605
+ package = self._create(
606
+ project_to_deploy=project_to_deploy,
607
+ package_name=package_name,
608
+ modules=modules,
609
+ slots=slots,
610
+ codebase=codebase,
611
+ is_global=is_global,
612
+ version=version,
613
+ service_config=service_config,
614
+ requirements=requirements,
615
+ package_type=package_type,
616
+ metadata=metadata
617
+ )
618
+ if checkout:
619
+ self.checkout(package=package)
620
+ except Exception:
621
+ if delete_codebase:
622
+ project_to_deploy.items.delete(item_id=codebase.item_id)
623
+ raise
624
+
625
+ logger.info("Package name: {!r}, id: {!r} has been added to project name: {!r}, id: {!r}".format(
626
+ package.name,
627
+ package.id,
628
+ package.project.name,
629
+ package.project.id))
630
+
631
+ if service_update:
632
+ service = package.services.get(service_name=package.name)
633
+ service.package_revision = package.version
634
+ service.update()
635
+ return package
636
+
637
+ def _create(self,
638
+ project_to_deploy: entities.Project = None,
639
+ codebase: typing.Union[entities.GitCodebase, entities.ItemCodebase, entities.FilesystemCodebase] = None,
640
+ is_global: bool = None,
641
+ package_name: str = entities.package_defaults.DEFAULT_PACKAGE_NAME,
642
+ modules: typing.List[entities.PackageModule] = None,
643
+ version: str = None,
644
+ service_config: dict = None,
645
+ slots: typing.List[entities.PackageSlot] = None,
646
+ requirements: typing.List[entities.PackageRequirement] = None,
647
+ package_type='faas',
648
+ metadata=None
649
+ ) -> entities.Package:
650
+ """
651
+ Create a package in platform.
652
+
653
+ :param project_to_deploy:
654
+ :param dtlpy.entities.codebase.Codebase codebase: codebase object
655
+ :param bool is_global: is package is global or local
656
+ :param str package_name: optional - default: 'default package'
657
+ :param list modules: optional - PackageModules Entity
658
+ :param str version: semver version of the package
659
+ :param dict service_config : Service object as dict. Contains the spec of the default service to create.
660
+ :param list slots: optional - list of slots PackageSlot of the package
661
+ :param list requirements: requirements - list of package requirements
662
+ :param str package_type: default 'faas', options: 'app', 'ml
663
+ :param dict metadata: dictionary of system and user metadata
664
+ :return: Package object
665
+ :rtype: dtlpy.entities.package.Package
666
+ """
667
+ if modules is not None:
668
+ if not isinstance(modules, list):
669
+ modules = [modules]
670
+
671
+ if isinstance(modules[0], entities.PackageModule):
672
+ modules = [module.to_json() for module in modules]
673
+
674
+ if slots and isinstance(slots[0], entities.PackageSlot):
675
+ slots = [slot.to_json() for slot in slots]
676
+
677
+ if is_global is None:
678
+ is_global = False
679
+
680
+ payload = {'name': package_name,
681
+ 'global': is_global,
682
+ 'modules': modules,
683
+ 'slots': slots,
684
+ 'serviceConfig': service_config,
685
+ 'type': package_type
686
+ }
687
+
688
+ if codebase is not None:
689
+ payload['codebase'] = codebase.to_json()
690
+
691
+ if requirements is not None:
692
+ payload['requirements'] = [r.to_json() for r in requirements]
693
+
694
+ if version is not None:
695
+ payload['version'] = version
696
+
697
+ if metadata is not None:
698
+ payload['metadata'] = metadata
699
+
700
+ if project_to_deploy is not None:
701
+ payload['projectId'] = project_to_deploy.id
702
+ else:
703
+ raise exceptions.PlatformException('400', 'Repository must have a project to perform this action')
704
+
705
+ # request
706
+ success, response = self._client_api.gen_request(req_type='post',
707
+ path='/packages',
708
+ json_req=payload)
709
+
710
+ # exception handling
711
+ if not success:
712
+ raise exceptions.PlatformException(response)
713
+
714
+ # return entity
715
+ return entities.Package.from_json(_json=response.json(),
716
+ client_api=self._client_api,
717
+ project=project_to_deploy)
718
+
719
+ @_api_reference.add(path='/packages/{id}', method='delete')
720
+ def delete(self, package: entities.Package = None, package_name=None, package_id=None):
721
+ """
722
+ Delete a Package object.
723
+
724
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
725
+
726
+ :param dtlpy.entities.package.Package package: package entity
727
+ :param str package_id: package id
728
+ :param str package_name: package name
729
+ :return: True if success
730
+ :rtype: bool
731
+
732
+ **Example**:
733
+
734
+ .. code-block:: python
735
+
736
+ project.packages.delete(package_name='package_name')
737
+ """
738
+ # get id and name
739
+ if package_name is None or package_id is None:
740
+ if package is None:
741
+ package = self.get(package_id=package_id, package_name=package_name)
742
+ package_id = package.id
743
+
744
+ # check if project exist
745
+ project_exists = True
746
+ if self._project is None:
747
+ try:
748
+ if package is not None and package.project is not None:
749
+ self._project = package.project
750
+ else:
751
+ self._project = repositories.Projects(client_api=self._client_api).get(
752
+ project_id=package.project_id)
753
+ except exceptions.NotFound:
754
+ project_exists = False
755
+
756
+ if project_exists:
757
+ # TODO can remove? in transactor?
758
+ try:
759
+ # create codebases repo
760
+ codebases = repositories.Codebases(client_api=self._client_api, project=self._project)
761
+ # get package codebases
762
+ codebase_pages = codebases.list_versions(codebase_name=package_name)
763
+ for codebase_page in codebase_pages:
764
+ for codebase in codebase_page:
765
+ codebase.delete()
766
+ except exceptions.Forbidden:
767
+ logger.debug('Failed to delete code-bases. Continue without')
768
+
769
+ # request
770
+ success, response = self._client_api.gen_request(
771
+ req_type="delete",
772
+ path="/packages/{}".format(package_id)
773
+ )
774
+
775
+ # exception handling
776
+ if not success:
777
+ raise exceptions.PlatformException(response)
778
+
779
+ # return results
780
+ return True
781
+
782
+ @_api_reference.add(path='/packages/{id}', method='patch')
783
+ def update(self, package: entities.Package, revision_increment: str = None) -> entities.Package:
784
+ """
785
+ Update Package changes to the platform.
786
+
787
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
788
+
789
+ :param dtlpy.entities.package.Package package:
790
+ :param revision_increment: optional - str - version bumping method - major/minor/patch - default = None
791
+ :return: Package object
792
+ :rtype: dtlpy.entities.package.Package
793
+
794
+ **Example**:
795
+
796
+ .. code-block:: python
797
+
798
+ project.packages.delete(package='package_entity')
799
+ """
800
+
801
+ if revision_increment is not None and isinstance(package.version, str) and len(package.version.split('.')) == 3:
802
+ major, minor, patch = package.version.split('.')
803
+ if revision_increment == 'patch':
804
+ patch = int(patch) + 1
805
+ elif revision_increment == 'minor':
806
+ minor = int(minor) + 1
807
+ elif revision_increment == 'major':
808
+ major = int(major) + 1
809
+ package.version = '{}.{}.{}'.format(major, minor, patch)
810
+
811
+ # payload
812
+ payload = package.to_json()
813
+
814
+ # request
815
+ success, response = self._client_api.gen_request(req_type='patch',
816
+ path='/packages/{}'.format(package.id),
817
+ json_req=payload)
818
+
819
+ # exception handling
820
+ if not success:
821
+ raise exceptions.PlatformException(response)
822
+
823
+ # return entity
824
+ return entities.Package.from_json(_json=response.json(),
825
+ client_api=self._client_api,
826
+ project=self._project)
827
+
828
+ def deploy(self,
829
+ package_id: str = None,
830
+ package_name: str = None,
831
+ package: entities.Package = None,
832
+ service_name: str = None,
833
+ project_id: str = None,
834
+ revision: str or int = None,
835
+ init_input: typing.Union[typing.List[entities.FunctionIO], entities.FunctionIO, dict] = None,
836
+ runtime: typing.Union[entities.KubernetesRuntime, dict] = None,
837
+ sdk_version: str = None,
838
+ agent_versions: dict = None,
839
+ bot: typing.Union[entities.Bot, str] = None,
840
+ pod_type: entities.InstanceCatalog = None,
841
+ verify: bool = True,
842
+ checkout: bool = False,
843
+ module_name: str = None,
844
+ run_execution_as_process: bool = None,
845
+ execution_timeout: int = None,
846
+ drain_time: int = None,
847
+ on_reset: str = None,
848
+ max_attempts: int = None,
849
+ force: bool = False,
850
+ secrets: list = None,
851
+ **kwargs) -> entities.Service:
852
+ """
853
+ Deploy a package. A service is required to run the code in your package.
854
+
855
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
856
+
857
+ :param str package_id: package id
858
+ :param str package_name: package name
859
+ :param dtlpy.entities.package.Package package: package entity
860
+ :param str service_name: service name
861
+ :param str project_id: project id
862
+ :param str revision: package revision - default=latest
863
+ :param init_input: config to run at startup
864
+ :param dict runtime: runtime resources
865
+ :param str sdk_version: - optional - string - sdk version
866
+ :param dict agent_versions: - dictionary - - optional -versions of sdk, agent runner and agent proxy
867
+ :param str bot: bot email
868
+ :param str pod_type: pod type dl.InstanceCatalog
869
+ :param bool verify: verify the inputs
870
+ :param bool checkout: checkout
871
+ :param str module_name: module name
872
+ :param bool run_execution_as_process: run execution as process
873
+ :param int execution_timeout: execution timeout
874
+ :param int drain_time: drain time
875
+ :param str on_reset: on reset
876
+ :param int max_attempts: Maximum execution retries in-case of a service reset
877
+ :param bool force: optional - terminate old replicas immediately
878
+ :param list secrets: list of the integrations ids
879
+ :return: Service object
880
+ :rtype: dtlpy.entities.service.Service
881
+
882
+ **Example**:
883
+
884
+ .. code-block:: python
885
+
886
+ project.packages.deploy(service_name=package_name,
887
+ execution_timeout=3 * 60 * 60,
888
+ module_name=module.name,
889
+ runtime=dl.KubernetesRuntime(
890
+ concurrency=10,
891
+ pod_type=dl.InstanceCatalog.REGULAR_S,
892
+ autoscaler=dl.KubernetesRabbitmqAutoscaler(
893
+ min_replicas=1,
894
+ max_replicas=20,
895
+ queue_length=20
896
+ )
897
+ )
898
+ )
899
+ """
900
+
901
+ if package is None:
902
+ package = self.get(package_id=package_id, package_name=package_name)
903
+
904
+ return package.services.deploy(package=package,
905
+ service_name=service_name,
906
+ revision=revision,
907
+ init_input=init_input,
908
+ runtime=runtime,
909
+ sdk_version=sdk_version,
910
+ agent_versions=agent_versions,
911
+ project_id=project_id,
912
+ pod_type=pod_type,
913
+ bot=bot,
914
+ verify=verify,
915
+ module_name=module_name,
916
+ checkout=checkout,
917
+ jwt_forward=kwargs.get('jwt_forward', None),
918
+ is_global=kwargs.get('is_global', None),
919
+ run_execution_as_process=run_execution_as_process,
920
+ execution_timeout=execution_timeout,
921
+ drain_time=drain_time,
922
+ on_reset=on_reset,
923
+ max_attempts=max_attempts,
924
+ force=force,
925
+ secrets=secrets
926
+ )
927
+
928
+ def deploy_from_file(self, project, json_filepath):
929
+ """
930
+ Deploy package and service from a JSON file.
931
+
932
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
933
+
934
+ :param dtlpy.entities.project.Project project: project entity
935
+ :param str json_filepath: path of the file to deploy
936
+ :return: the package and the services
937
+
938
+ **Example**:
939
+
940
+ .. code-block:: python
941
+
942
+ project.packages.deploy_from_file(project='project_entity', json_filepath='json_filepath')
943
+ """
944
+ with open(json_filepath, 'r') as f:
945
+ data = json.load(f)
946
+
947
+ package = project.packages.push(
948
+ package_name=data['name'],
949
+ modules=data['modules'],
950
+ src_path=os.path.split(json_filepath)[0]
951
+ )
952
+ deployed_services = list()
953
+ if 'services' in data:
954
+ for service_json in data['services']:
955
+ try:
956
+ # update
957
+ service_old = project.services.get(service_name=service_json['name'])
958
+ except exceptions.NotFound:
959
+ # create
960
+ service_old = package.services.deploy(service_name=service_json['name'],
961
+ module_name=data['modules'][0]['name'])
962
+ triggers = service_json.pop('triggers', None)
963
+ artifacts = service_json.pop('artifacts', None)
964
+ to_update, service = self.__compare_and_update_service_configurations(service=service_old,
965
+ service_json=service_json)
966
+ if to_update:
967
+ service.package_revision = package.version
968
+ service.update()
969
+ if triggers:
970
+ self.__compare_and_update_trigger_configurations(service, triggers)
971
+ if artifacts:
972
+ self.__compare_and_upload_artifacts(artifacts, package)
973
+ deployed_services.append(service)
974
+ else:
975
+ logger.warning(msg='Package JSON does not have services.')
976
+ return deployed_services, package
977
+
978
+ def __update_dict_recursive(self, d, u):
979
+ for k, v in u.items():
980
+ if isinstance(v, collections.abc.Mapping):
981
+ inner_dict = d.get(k, {})
982
+ if inner_dict is None:
983
+ inner_dict = {}
984
+ d[k] = self.__update_dict_recursive(inner_dict, v)
985
+ else:
986
+ d[k] = v
987
+ return d
988
+
989
+ def __compare_and_update_trigger_configurations(self, service, trigger_list):
990
+ """
991
+ :param service:
992
+ :param trigger_list:
993
+ """
994
+ service_triggers = service.triggers.list().all()
995
+ triggers_dict = dict()
996
+ for trigger in service_triggers:
997
+ triggers_dict[trigger.name] = trigger
998
+
999
+ for trigger_json in trigger_list:
1000
+ trigger_name = trigger_json['name']
1001
+ trigger_spec = trigger_json['spec']
1002
+ if trigger_name not in triggers_dict:
1003
+ # create
1004
+ service.triggers.create(
1005
+ # general
1006
+ name=trigger_name,
1007
+ function_name=trigger_spec['operation']['functionName'],
1008
+ trigger_type=trigger_json['type'],
1009
+ # event
1010
+ scope=trigger_json.get('scope', None),
1011
+ is_global=trigger_json.get('global', None),
1012
+ filters=trigger_spec.get('filter', None),
1013
+ resource=trigger_spec.get('resource', None),
1014
+ execution_mode=trigger_spec.get('executionMode', None),
1015
+ actions=trigger_spec.get('actions', None),
1016
+ # cron
1017
+ start_at=trigger_spec.get('startAt', None),
1018
+ end_at=trigger_spec.get('endAt', None),
1019
+ cron=trigger_spec.get('cron', None),
1020
+ )
1021
+ else:
1022
+ existing_trigger = triggers_dict[trigger_name]
1023
+ # check diff
1024
+ _json = existing_trigger.to_json()
1025
+ # pop unmatched fields
1026
+ _ = _json['spec']['operation'].pop('serviceId')
1027
+ _new_json = _json.copy()
1028
+ # update fields from infra configurations
1029
+ self.__update_dict_recursive(_new_json, trigger_json)
1030
+ # check diffs
1031
+ diffs = miscellaneous.DictDiffer.diff(_json, _new_json)
1032
+ if diffs:
1033
+ updated_trigger = existing_trigger.from_json(_json=_new_json,
1034
+ client_api=service._client_api,
1035
+ service=service,
1036
+ project=service._project)
1037
+ updated_trigger.update()
1038
+
1039
+ def __compare_and_update_service_configurations(self, service, service_json):
1040
+ """
1041
+ :param service:
1042
+ :param service_json:
1043
+ """
1044
+ # take json configuration from service
1045
+ _json = service.to_json()
1046
+ _new_json = _json.copy()
1047
+ # update fields from infra configurations
1048
+ self.__update_dict_recursive(_new_json, service_json)
1049
+ # check diffs
1050
+ diffs = miscellaneous.DictDiffer.diff(_json, _new_json)
1051
+ to_update = False
1052
+ if diffs:
1053
+ # build back service from json
1054
+ to_update = True
1055
+ service = service.from_json(_json=_new_json,
1056
+ client_api=service._client_api,
1057
+ package=service._package,
1058
+ project=service._project)
1059
+ return to_update, service
1060
+
1061
+ def __compare_and_upload_artifacts(self, artifacts, package):
1062
+ """
1063
+ :param artifacts:
1064
+ :param package:
1065
+ """
1066
+ for artifact in artifacts:
1067
+ # create/get .dataloop dir
1068
+ cwd = os.getcwd()
1069
+ dl_dir = os.path.join(cwd, '.dataloop')
1070
+ if not os.path.isdir(dl_dir):
1071
+ os.mkdir(dl_dir)
1072
+
1073
+ directory = os.path.abspath(artifact)
1074
+ m = hashlib.md5()
1075
+ with open(directory, 'rb') as f:
1076
+ for chunk in iter(lambda: f.read(4096), b''):
1077
+ m.update(chunk)
1078
+ zip_md = m.hexdigest()
1079
+ artifacts_list = package.artifacts.list()
1080
+ for art in artifacts_list:
1081
+ if art.metadata['system']['md5'] == zip_md:
1082
+ return
1083
+
1084
+ artifact_item = package.artifacts.upload(filepath=directory,
1085
+ package_name=package.name,
1086
+ package=package,
1087
+ overwrite=True, )
1088
+
1089
+ artifact_item.metadata['system']['md5'] = zip_md
1090
+ artifact_item.update(True)
1091
+
1092
+ @staticmethod
1093
+ def _package_json_generator(package_catalog, package_name):
1094
+ """
1095
+ :param package_catalog:
1096
+ :param package_name:
1097
+ """
1098
+ if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1099
+ with open(assets.paths.ASSETS_PACKAGE_FILEPATH, 'r') as f:
1100
+ package_asset = json.load(f)
1101
+ package_asset['name'] = package_name
1102
+ return package_asset
1103
+
1104
+ item_input = entities.FunctionIO(name='item', type='Item')
1105
+ annotation_input = entities.FunctionIO(name='annotation', type='Annotation')
1106
+ dataset_input = entities.FunctionIO(name='dataset', type='Dataset')
1107
+ json_input = entities.FunctionIO(name='config', type='Json')
1108
+ query_input = entities.FunctionIO(name='query', type='Json')
1109
+
1110
+ func = entities.PackageFunction(name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME)
1111
+ if package_catalog in [PackageCatalog.SINGLE_FUNCTION_ITEM, PackageCatalog.SINGLE_FUNCTION_ITEM_WITH_TRIGGER]:
1112
+ func.inputs = [item_input]
1113
+ modules = entities.PackageModule(functions=[func])
1114
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ANNOTATION,
1115
+ PackageCatalog.SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER]:
1116
+ func.inputs = [annotation_input]
1117
+ modules = entities.PackageModule(functions=[func])
1118
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_DATASET,
1119
+ PackageCatalog.SINGLE_FUNCTION_DATASET_WITH_TRIGGER]:
1120
+ func.inputs = [dataset_input]
1121
+ modules = entities.PackageModule(functions=[func])
1122
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_NO_INPUT]:
1123
+ modules = entities.PackageModule(functions=[func])
1124
+ elif package_catalog == PackageCatalog.SINGLE_FUNCTION_JSON:
1125
+ func.inputs = json_input
1126
+ modules = entities.PackageModule(functions=[func])
1127
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ITEM, PackageCatalog.MULTI_FUNCTION_ITEM_WITH_TRIGGERS]:
1128
+ func.inputs = [item_input]
1129
+ func.name = 'first_method'
1130
+ second_func = deepcopy(func)
1131
+ second_func.name = 'second_method'
1132
+ modules = entities.PackageModule(functions=[func, second_func])
1133
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ANNOTATION,
1134
+ PackageCatalog.MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS]:
1135
+ func.inputs = [annotation_input]
1136
+ func.name = 'first_method'
1137
+ second_func = deepcopy(func)
1138
+ second_func.name = 'second_method'
1139
+ modules = entities.PackageModule(functions=[func, second_func])
1140
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_DATASET,
1141
+ PackageCatalog.MULTI_FUNCTION_DATASET_WITH_TRIGGERS]:
1142
+ func.inputs = [dataset_input]
1143
+ func.name = 'first_method'
1144
+ second_func = deepcopy(func)
1145
+ second_func.name = 'second_method'
1146
+ modules = entities.PackageModule(functions=[func, second_func])
1147
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_JSON]:
1148
+ func.inputs = [json_input]
1149
+ func.name = 'first_method'
1150
+ second_func = deepcopy(func)
1151
+ second_func.name = 'second_method'
1152
+ modules = entities.PackageModule(functions=[func, second_func])
1153
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_NO_INPUT]:
1154
+ func.name = 'first_method'
1155
+ second_func = deepcopy(func)
1156
+ second_func.name = 'second_method'
1157
+ modules = entities.PackageModule(functions=[func, second_func])
1158
+ elif package_catalog in [PackageCatalog.MULTI_MODULE, PackageCatalog.MULTI_MODULE_WITH_TRIGGER]:
1159
+ func.inputs = [item_input]
1160
+ module_a = entities.PackageModule(functions=[func],
1161
+ name='first_module',
1162
+ entry_point='first_module_class.py')
1163
+ module_b = entities.PackageModule(functions=[func],
1164
+ name='second_module',
1165
+ entry_point='second_module_class.py')
1166
+ modules = [module_a, module_b]
1167
+ elif package_catalog == PackageCatalog.SINGLE_FUNCTION_MULTI_INPUT:
1168
+ func.inputs = [item_input, dataset_input, json_input, annotation_input]
1169
+ modules = entities.PackageModule(functions=[func])
1170
+ elif package_catalog == PackageCatalog.CONVERTER_FUNCTION:
1171
+ func.inputs = [dataset_input, query_input]
1172
+ modules = entities.PackageModule(functions=[func])
1173
+ else:
1174
+ raise exceptions.PlatformException('404', 'Unknown package catalog type: {}'.format(package_catalog))
1175
+
1176
+ if not isinstance(modules, list):
1177
+ modules = [modules]
1178
+
1179
+ _json = {'name': package_name,
1180
+ 'modules': [module.to_json() for module in modules]}
1181
+
1182
+ return _json
1183
+
1184
+ @staticmethod
1185
+ def build_trigger_dict(actions,
1186
+ name='default_module',
1187
+ filters=None,
1188
+ function='run',
1189
+ execution_mode: entities.TriggerExecutionMode = 'Once',
1190
+ type_t: entities.TriggerType = "Event"):
1191
+ """
1192
+ Build a trigger dictionary to trigger FaaS.
1193
+ Read more about `FaaS Triggers <https://developers.dataloop.ai/tutorials/faas/auto_annotate/chapter/#trigger-the-service>`_.
1194
+
1195
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
1196
+
1197
+ :param actions: list of dl.TriggerAction
1198
+ :param str name: trigger name
1199
+ :param dtlpy.entities.filters.Filters filters: Filters entity or a dictionary containing filters parameters
1200
+ :param str function: function name
1201
+ :param str execution_mode: execution mode dl.TriggerExecutionMode
1202
+ :param str type_t: trigger type dl.TriggerType
1203
+ :return: trigger dict
1204
+ :rtype: dict
1205
+
1206
+ **Example**:
1207
+
1208
+ .. code-block:: python
1209
+
1210
+ project.packages.build_trigger_dict(actions=dl.TriggerAction.CREATED,
1211
+ function='run',
1212
+ execution_mode=dl.TriggerExecutionMode.ONCE)
1213
+ """
1214
+ if not isinstance(actions, list):
1215
+ actions = [actions]
1216
+
1217
+ if not filters:
1218
+ filters = dict()
1219
+
1220
+ trigger_dict = {
1221
+ "name": name,
1222
+ "type": type_t,
1223
+ "spec": {
1224
+ "filter": filters,
1225
+ "executionMode": execution_mode,
1226
+ "resource": "Item",
1227
+ "actions": actions,
1228
+ "input": {},
1229
+ "operation": {
1230
+ "type": "function",
1231
+ "functionName": function
1232
+ }
1233
+ }
1234
+ }
1235
+ return trigger_dict
1236
+
1237
+ @staticmethod
1238
+ def _service_json_generator(package_catalog, service_name):
1239
+ """
1240
+ :param package_catalog:
1241
+ :param service_name:
1242
+ """
1243
+ triggers = list()
1244
+ with open(assets.paths.ASSETS_PACKAGE_FILEPATH, 'r') as f:
1245
+ service_json = json.load(f)["services"][0]
1246
+ if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1247
+ triggers = service_json['triggers']
1248
+ elif 'triggers' in package_catalog:
1249
+ trigger_a = Packages.build_trigger_dict(name='first_trigger',
1250
+ filters=dict(),
1251
+ actions=['Created'],
1252
+ function='first_method',
1253
+ execution_mode=entities.TriggerExecutionMode.ONCE)
1254
+ trigger_b = Packages.build_trigger_dict(name='second_trigger',
1255
+ filters=dict(),
1256
+ actions=['Created'],
1257
+ function='second_method',
1258
+ execution_mode=entities.TriggerExecutionMode.ONCE)
1259
+ if 'item' in package_catalog:
1260
+ trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Item'
1261
+ if 'dataset' in package_catalog:
1262
+ trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Dataset'
1263
+ if 'annotation' in package_catalog:
1264
+ trigger_a['spec']['resource'] = trigger_b['spec']['resource'] = 'Annotation'
1265
+ triggers += [trigger_a, trigger_b]
1266
+ elif 'trigger' in package_catalog:
1267
+ trigger = Packages.build_trigger_dict(name='triggername',
1268
+ filters=dict(),
1269
+ actions=[],
1270
+ function=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1271
+ execution_mode=entities.TriggerExecutionMode.ONCE)
1272
+ if 'item' in package_catalog:
1273
+ trigger['spec']['resource'] = 'Item'
1274
+ if 'dataset' in package_catalog:
1275
+ trigger['spec']['resource'] = 'Dataset'
1276
+ if 'annotation' in package_catalog:
1277
+ trigger['spec']['resource'] = 'Annotation'
1278
+ triggers += [trigger]
1279
+
1280
+ if service_name is not None:
1281
+ service_json['name'] = service_name
1282
+ if 'multi_module' in package_catalog:
1283
+ service_json['moduleName'] = 'first_module'
1284
+
1285
+ service_json['triggers'] = triggers
1286
+
1287
+ return service_json
1288
+
1289
+ @staticmethod
1290
+ def generate(name=None,
1291
+ src_path: str = None,
1292
+ service_name: str = None,
1293
+ package_type=PackageCatalog.DEFAULT_PACKAGE_TYPE):
1294
+ """
1295
+ Generate a new package. Provide a file path to a JSON file with all the details of the package and service to generate the package.
1296
+
1297
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
1298
+
1299
+ :param str name: name
1300
+ :param str src_path: source file path
1301
+ :param str service_name: service name
1302
+ :param str package_type: package type from PackageCatalog
1303
+
1304
+ **Example**:
1305
+
1306
+ .. code-block:: python
1307
+
1308
+ project.packages.generate(name='package_name',
1309
+ src_path='src_path')
1310
+
1311
+ """
1312
+ # name
1313
+ if name is None:
1314
+ name = 'default-package'
1315
+ if service_name is None:
1316
+ service_name = 'default-service'
1317
+
1318
+ # src path
1319
+ if src_path is None:
1320
+ src_path = os.getcwd()
1321
+
1322
+ package_asset = Packages._package_json_generator(package_name=name,
1323
+ package_catalog=package_type)
1324
+ services_asset = Packages._service_json_generator(package_catalog=package_type,
1325
+ service_name=service_name)
1326
+ package_asset["services"] = [services_asset]
1327
+ to_generate = [
1328
+ {'src': assets.paths.ASSETS_GITIGNORE_FILEPATH, 'dst': os.path.join(src_path, '.gitignore'),
1329
+ 'type': 'copy'},
1330
+ {'src': package_asset,
1331
+ 'dst': os.path.join(src_path, assets.paths.PACKAGE_FILENAME), 'type': 'json'}
1332
+ ]
1333
+
1334
+ if package_type == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1335
+ to_generate.append({'src': assets.paths.ASSETS_MOCK_FILEPATH,
1336
+ 'dst': os.path.join(src_path, assets.paths.MOCK_FILENAME), 'type': 'copy'})
1337
+ else:
1338
+ module = entities.PackageModule.from_json(package_asset['modules'][0])
1339
+ function_name = module.functions[0].name
1340
+
1341
+ to_generate.append({'src': Packages._mock_json_generator(module=module, function_name=function_name),
1342
+ 'dst': os.path.join(src_path, assets.paths.MOCK_FILENAME), 'type': 'json'})
1343
+
1344
+ main_file_paths = Packages._entry_point_generator(package_catalog=package_type)
1345
+ if len(main_file_paths) == 1:
1346
+ to_generate.append({'src': main_file_paths[0],
1347
+ 'dst': os.path.join(src_path, assets.paths.MAIN_FILENAME),
1348
+ 'type': 'copy'})
1349
+ else:
1350
+ to_generate += [{'src': main_file_paths[0],
1351
+ 'dst': os.path.join(src_path, assets.paths.MODULE_A_FILENAME),
1352
+ 'type': 'copy'},
1353
+ {'src': main_file_paths[1],
1354
+ 'dst': os.path.join(src_path, assets.paths.MODULE_B_FILENAME),
1355
+ 'type': 'copy'}
1356
+ ]
1357
+
1358
+ for job in to_generate:
1359
+ if not os.path.isfile(job['dst']):
1360
+ if job['type'] == 'copy':
1361
+ copyfile(job['src'], job['dst'])
1362
+ elif job['type'] == 'json':
1363
+ with open(job['dst'], 'w') as f:
1364
+ json.dump(job['src'], f, indent=2)
1365
+
1366
+ logger.info('Successfully generated package')
1367
+
1368
+ def build(self, package: entities.Package, module_name=None, init_inputs=None, local_path=None, from_local=None):
1369
+ """
1370
+ Instantiate a module from the package code. Returns a loaded instance of the runner class
1371
+
1372
+ :param package: Package entity
1373
+ :param module_name: Name of the module to build the runner class
1374
+ :param str init_inputs: dictionary of the class init variables (if exists). will be used to init the module class
1375
+ :param str local_path: local path of the package (if from_local=False - codebase will be downloaded)
1376
+ :param bool from_local: bool. if true - codebase will not be downloaded (only use local files)
1377
+ :return: dl.BaseServiceRunner
1378
+ """
1379
+ if module_name is None:
1380
+ module_name = entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME
1381
+ if init_inputs is None:
1382
+ init_inputs = dict()
1383
+ if package.codebase is None:
1384
+ raise exceptions.PlatformException(error='2001',
1385
+ message="Package {!r} has no codebase. unable to build a module".format(
1386
+ package.name))
1387
+
1388
+ if from_local is None:
1389
+ from_local = package.codebase.is_local
1390
+
1391
+ if not from_local:
1392
+ # Not local => download codebase
1393
+ if package.codebase.is_local:
1394
+ raise RuntimeError("using local codebase: {}. Cannot use from_local=False".
1395
+ format(package.codebase.to_json()))
1396
+ local_path = self.pull(package=package, local_path=local_path)
1397
+
1398
+ sys.path.append(local_path) # TODO: is it the best way to use the imports?
1399
+ # load module from path
1400
+ module = [module for module in package.modules if module.name == module_name]
1401
+ if len(module) != 1:
1402
+ raise ValueError('Cannot find module from package. trying to load: {!r}, available modules: {}'.format(
1403
+ module_name,
1404
+ [module.name for module in package.modules]
1405
+ ))
1406
+ module = module[0]
1407
+ entry_point = os.path.join(local_path, module.entry_point)
1408
+ class_name = module.class_name
1409
+ if not os.path.isfile(entry_point):
1410
+ raise ValueError('Module entry point file is missing: {}'.format(entry_point))
1411
+ spec = importlib.util.spec_from_file_location(class_name, entry_point)
1412
+ if spec is None:
1413
+ raise ValueError('Cant load class ModelAdapter from entry point: {}'.format(entry_point))
1414
+ package_module = importlib.util.module_from_spec(spec)
1415
+ spec.loader.exec_module(package_module)
1416
+ package_module_cls = getattr(package_module, class_name)
1417
+ package_module = package_module_cls(**init_inputs)
1418
+ logger.info('Model adapter loaded successfully!')
1419
+ return package_module
1420
+
1421
+ @staticmethod
1422
+ def _mock_json_generator(module: entities.PackageModule, function_name):
1423
+ """
1424
+ :param module:
1425
+ :param function_name:
1426
+ """
1427
+ _json = dict(function_name=function_name, module_name=module.name)
1428
+ funcs = [func for func in module.functions if func.name == function_name]
1429
+ if len(funcs) == 1:
1430
+ func = funcs[0]
1431
+ else:
1432
+ raise exceptions.PlatformException('400', 'Other than 1 functions by the name of: {}'.format(function_name))
1433
+ _json['config'] = {inpt.name: entities.Package._mockify_input(input_type=inpt.type)
1434
+ for inpt in module.init_inputs}
1435
+ _json['inputs'] = [{'name': inpt.name, 'value': entities.Package._mockify_input(input_type=inpt.type)}
1436
+ for inpt in func.inputs]
1437
+ return _json
1438
+
1439
+ @staticmethod
1440
+ def _entry_point_generator(package_catalog):
1441
+ """
1442
+ :param package_catalog:
1443
+ """
1444
+ if package_catalog == PackageCatalog.DEFAULT_PACKAGE_TYPE:
1445
+ paths_to_service_runner = assets.paths.ASSETS_MAIN_FILEPATH
1446
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ITEM, PackageCatalog.SINGLE_FUNCTION_ITEM_WITH_TRIGGER]:
1447
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH
1448
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_ANNOTATION,
1449
+ PackageCatalog.SINGLE_FUNCTION_ANNOTATION_WITH_TRIGGER]:
1450
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_ANNOTATION_SR_PATH
1451
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_DATASET,
1452
+ PackageCatalog.SINGLE_FUNCTION_DATASET_WITH_TRIGGER]:
1453
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_DATASET_SR_PATH
1454
+ elif package_catalog in [PackageCatalog.SINGLE_FUNCTION_NO_INPUT]:
1455
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_SR_PATH
1456
+ elif package_catalog == PackageCatalog.SINGLE_FUNCTION_JSON:
1457
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_JSON_SR_PATH
1458
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ITEM, PackageCatalog.MULTI_FUNCTION_ITEM_WITH_TRIGGERS]:
1459
+ paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_ITEM_SR_PATH
1460
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_ANNOTATION,
1461
+ PackageCatalog.MULTI_FUNCTION_ANNOTATION_WITH_TRIGGERS]:
1462
+ paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_ANNOTATION_SR_PATH
1463
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_DATASET,
1464
+ PackageCatalog.MULTI_FUNCTION_DATASET_WITH_TRIGGERS]:
1465
+ paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_DATASET_SR_PATH
1466
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_JSON]:
1467
+ paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_JSON_SR_PATH
1468
+ elif package_catalog in [PackageCatalog.MULTI_FUNCTION_NO_INPUT]:
1469
+ paths_to_service_runner = assets.service_runner_paths.MULTI_METHOD_SR_PATH
1470
+ elif package_catalog in [PackageCatalog.MULTI_MODULE, PackageCatalog.MULTI_MODULE_WITH_TRIGGER]:
1471
+ paths_to_service_runner = [assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH,
1472
+ assets.service_runner_paths.SINGLE_METHOD_ITEM_SR_PATH]
1473
+ elif package_catalog == PackageCatalog.SINGLE_FUNCTION_MULTI_INPUT:
1474
+ paths_to_service_runner = assets.service_runner_paths.SINGLE_METHOD_MULTI_INPUT_SR_PATH
1475
+ elif package_catalog == PackageCatalog.CONVERTER_FUNCTION:
1476
+ paths_to_service_runner = assets.service_runner_paths.CONVERTER_SR_PATH
1477
+ else:
1478
+ raise exceptions.PlatformException('404', 'Unknown package catalog type: {}'.format(package_catalog))
1479
+
1480
+ if not isinstance(paths_to_service_runner, list):
1481
+ paths_to_service_runner = [paths_to_service_runner]
1482
+
1483
+ return paths_to_service_runner
1484
+
1485
+ @staticmethod
1486
+ def is_multithread(inputs):
1487
+ is_multi = False
1488
+ if len(inputs) > 0 and isinstance(inputs[0], list):
1489
+ is_multi = True
1490
+
1491
+ if is_multi:
1492
+ for single_input in inputs:
1493
+ if not isinstance(single_input, list):
1494
+ raise exceptions.PlatformException('400', 'mock.json inputs can be either list of dictionaries '
1495
+ 'or list of lists')
1496
+
1497
+ return is_multi
1498
+
1499
+ def test_local_package(self,
1500
+ cwd: str = None,
1501
+ concurrency: int = None,
1502
+ package: entities.Package = None,
1503
+ module_name: str = entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
1504
+ function_name: str = entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1505
+ class_name: str = entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
1506
+ entry_point: str = entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
1507
+ mock_file_path: str = None):
1508
+ """
1509
+ Test local package in local environment.
1510
+
1511
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
1512
+
1513
+ :param str cwd: path to the file
1514
+ :param int concurrency: the concurrency of the test
1515
+ :param dtlpy.entities.package.Package package: entities.package
1516
+ :param str module_name: module name
1517
+ :param str function_name: function name
1518
+ :param str class_name: class name
1519
+ :param str entry_point: the file to run like main.py
1520
+ :param str mock_file_path: the mock file that have the inputs
1521
+ :return: list created by the function that tested the output
1522
+ :rtype: list
1523
+
1524
+ **Example**:
1525
+
1526
+ .. code-block:: python
1527
+
1528
+ project.packages.test_local_package(cwd='path_to_package',
1529
+ package='package_entity',
1530
+ function_name='run')
1531
+ """
1532
+ if cwd is None:
1533
+ cwd = os.getcwd()
1534
+ if mock_file_path is None:
1535
+ mock_file_path = assets.paths.MOCK_FILENAME
1536
+ with open(os.path.join(cwd, mock_file_path), 'r') as f:
1537
+ mock_json = json.load(f)
1538
+ is_multithread = self.is_multithread(inputs=mock_json['inputs'])
1539
+
1540
+ local_runner = LocalServiceRunner(self._client_api,
1541
+ packages=self,
1542
+ cwd=cwd,
1543
+ package=package,
1544
+ multithreading=is_multithread,
1545
+ concurrency=concurrency,
1546
+ module_name=module_name,
1547
+ function_name=function_name,
1548
+ class_name=class_name,
1549
+ entry_point=entry_point,
1550
+ mock_file_path=mock_file_path)
1551
+
1552
+ if self._project is None:
1553
+ try:
1554
+ project = repositories.Projects(client_api=self._client_api).get()
1555
+ except Exception:
1556
+ project = None
1557
+ else:
1558
+ project = self.project
1559
+
1560
+ return local_runner.run_local_project(project=project)
1561
+
1562
+ def __get_from_cache(self) -> entities.Package:
1563
+ package = self._client_api.state_io.get('package')
1564
+ if package is not None:
1565
+ package = entities.Package.from_json(_json=package, client_api=self._client_api, project=self._project)
1566
+ return package
1567
+
1568
+ def checkout(self,
1569
+ package: entities.Package = None,
1570
+ package_id: str = None,
1571
+ package_name: str = None):
1572
+ """
1573
+ Checkout (switch) to a package.
1574
+
1575
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
1576
+
1577
+ :param dtlpy.entities.package.Package package: package entity
1578
+ :param str package_id: package id
1579
+ :param str package_name: package name
1580
+
1581
+ **Example**:
1582
+
1583
+ .. code-block:: python
1584
+
1585
+ project.packages.checkout(package='package_entity')
1586
+
1587
+ """
1588
+ if package is None:
1589
+ package = self.get(package_id=package_id, package_name=package_name)
1590
+ self._client_api.state_io.put('package', package.to_json())
1591
+ logger.info("Checked out to package {}".format(package.name))
1592
+
1593
+ @staticmethod
1594
+ def check_cls_arguments(cls, missing, function_name, function_inputs):
1595
+ """
1596
+ Check class arguments. This method checks that the package function is correct.
1597
+
1598
+ **Prerequisites**: You must be in the role of an *owner* or *developer*.
1599
+
1600
+ :param cls: packages class
1601
+ :param list missing: list of the missing params
1602
+ :param str function_name: name of function
1603
+ :param list function_inputs: list of function inputs
1604
+ """
1605
+ # input to function and inputs definitions match
1606
+ func = getattr(cls, function_name)
1607
+ func_inspect = inspect.getfullargspec(func)
1608
+ if not isinstance(function_inputs, list):
1609
+ function_inputs = [function_inputs]
1610
+ defined_input_names = [inp['name'] if isinstance(inp, dict) else inp.name for inp in function_inputs]
1611
+
1612
+ args_with_values = dict()
1613
+ # go over the defaults and inputs from end to start to map between them
1614
+ if func_inspect.defaults is not None:
1615
+ for i_value in range(-1, -len(func_inspect.defaults) - 1, -1):
1616
+ args_with_values[func_inspect.args[i_value]] = func_inspect.defaults[i_value]
1617
+
1618
+ for input_name in func_inspect.args:
1619
+ if input_name in ['self', 'progress', 'context']:
1620
+ continue
1621
+ if input_name not in defined_input_names:
1622
+ logger.warning('missing input name: "{}" in function definition: "{}"'.format(input_name,
1623
+ function_name))
1624
+ if input_name not in args_with_values:
1625
+ missing.append('missing input name: "{}" in function definition: "{}"'.format(input_name,
1626
+ function_name))
1627
+
1628
+ @staticmethod
1629
+ def _sanity_before_push(src_path, modules):
1630
+ """
1631
+ :param src_path:
1632
+ :param modules:
1633
+ """
1634
+ if modules is None:
1635
+ modules = [DEFAULT_PACKAGE_MODULE]
1636
+
1637
+ if not isinstance(modules, list):
1638
+ modules = [modules]
1639
+
1640
+ if not isinstance(modules[0], entities.PackageModule):
1641
+ modules = [entities.PackageModule.from_json(_json=module) for module in modules]
1642
+
1643
+ missing = list()
1644
+ for module in modules:
1645
+ entry_point = module.entry_point
1646
+ class_name = module.class_name
1647
+
1648
+ check_init = True
1649
+ functions = module.functions
1650
+ # check in inputs is a list
1651
+ if not isinstance(functions, list):
1652
+ functions = [functions]
1653
+
1654
+ for function in functions:
1655
+ # entry point exists
1656
+ entry_point_filepath = os.path.join(src_path, entry_point)
1657
+ if not os.path.isfile(entry_point_filepath):
1658
+ missing.append('missing entry point file: {}'.format(entry_point_filepath))
1659
+ continue
1660
+
1661
+ # class name in entry point
1662
+ try:
1663
+ spec = importlib.util.spec_from_file_location(class_name, entry_point_filepath)
1664
+ cls_def = importlib.util.module_from_spec(spec)
1665
+ spec.loader.exec_module(cls_def)
1666
+ except ImportError as e:
1667
+ logger.warning(
1668
+ 'Cannot run sanity check to find conflicts between package module and source code'
1669
+ 'because some packages in your source code are not installed in your local environment'.format(
1670
+ e.__str__())
1671
+ )
1672
+ return modules
1673
+
1674
+ if not hasattr(cls_def, class_name):
1675
+ missing.append('missing class: "{}" from file: "{}"'.format(class_name, entry_point_filepath))
1676
+ continue
1677
+
1678
+ # function in class
1679
+ cls = getattr(cls_def, class_name)
1680
+ if not hasattr(cls, function.name):
1681
+ missing.append(
1682
+ 'missing function: "{}" from class: "{}"'.format(function.name, entry_point_filepath))
1683
+ continue
1684
+
1685
+ if check_init:
1686
+ check_init = False
1687
+ # input to __init__ and inputs definitions match
1688
+ Packages.check_cls_arguments(cls=cls,
1689
+ missing=missing,
1690
+ function_name="__init__",
1691
+ function_inputs=module.init_inputs)
1692
+
1693
+ Packages.check_cls_arguments(cls=cls,
1694
+ missing=missing,
1695
+ function_name=function.name,
1696
+ function_inputs=function.inputs)
1697
+
1698
+ if len(missing) != 0:
1699
+ raise ValueError('There are conflicts between modules and source code:'
1700
+ '\n{}\n{}'.format(missing,
1701
+ 'Please fix conflicts or use flag ignore_sanity_check'))
1702
+
1703
+ return modules
1704
+
1705
+
1706
+ class LocalServiceRunner:
1707
+ """
1708
+ Service Runner Class
1709
+ """
1710
+
1711
+ def __init__(self,
1712
+ client_api: ApiClient,
1713
+ packages,
1714
+ cwd=None,
1715
+ multithreading=False,
1716
+ concurrency=10,
1717
+ package: entities.Package = None,
1718
+ module_name=entities.package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
1719
+ function_name=entities.package_defaults.DEFAULT_PACKAGE_FUNCTION_NAME,
1720
+ class_name=entities.package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
1721
+ entry_point=entities.package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
1722
+ mock_file_path=None):
1723
+ if cwd is None:
1724
+ self.cwd = os.getcwd()
1725
+ else:
1726
+ self.cwd = cwd
1727
+
1728
+ self._client_api = client_api
1729
+ self._packages = packages
1730
+ self.package_io = PackageIO(cwd=self.cwd)
1731
+ self.multithreading = multithreading
1732
+ self.concurrency = concurrency
1733
+ self._module_name = module_name
1734
+ self._function_name = function_name
1735
+ self._entry_point = entry_point
1736
+ self._class_name = class_name
1737
+ self.package = package
1738
+
1739
+ if mock_file_path is None:
1740
+ mock_file_path = 'mock.json'
1741
+ with open(os.path.join(self.cwd, mock_file_path), 'r') as f:
1742
+ self.mock_json = json.load(f)
1743
+
1744
+ @property
1745
+ def function_name(self):
1746
+ return self.mock_json.get('function_name', self._function_name)
1747
+
1748
+ @property
1749
+ def module_name(self):
1750
+ return self.mock_json.get('module_name', self._module_name)
1751
+
1752
+ @property
1753
+ def class_name(self):
1754
+ return self.mock_json.get('class_name', self._class_name)
1755
+
1756
+ @property
1757
+ def entry_point(self):
1758
+ return self.mock_json.get('entry_point', self._entry_point)
1759
+
1760
+ def get_mainpy_run_service(self):
1761
+ """
1762
+ Get mainpy run service
1763
+
1764
+ :return:
1765
+ """
1766
+ entry_point = os.path.join(self.cwd, self.entry_point)
1767
+ spec = importlib.util.spec_from_file_location(self.class_name, entry_point)
1768
+ cls_def = importlib.util.module_from_spec(spec)
1769
+ spec.loader.exec_module(cls_def)
1770
+ service_runner = getattr(cls_def, self.class_name)
1771
+
1772
+ if 'init_params' in self.mock_json:
1773
+ kwargs = self.mock_json.get('init_params')
1774
+ elif 'config' in self.mock_json:
1775
+ kwargs = self.mock_json.get('config')
1776
+ else:
1777
+ kwargs = dict()
1778
+
1779
+ return service_runner(**kwargs)
1780
+
1781
+ def run_local_project(self, project=None):
1782
+ """
1783
+ Run local project
1784
+
1785
+ :param project: project entity
1786
+ """
1787
+ package_runner = self.get_mainpy_run_service()
1788
+
1789
+ modules = self.package_io.get('modules') if self.package is None else [
1790
+ m.to_json() for m in self.package.modules
1791
+ ]
1792
+
1793
+ if isinstance(modules, list) and len(modules) > 0:
1794
+ try:
1795
+ module = [module for module in modules if module['name'] == self.module_name][0]
1796
+ except Exception:
1797
+ raise exceptions.PlatformException(
1798
+ '400', 'Module {} does not exist in package modules'.format(self.module_name)
1799
+ )
1800
+ else:
1801
+ module = DEFAULT_PACKAGE_MODULE.to_json()
1802
+
1803
+ if isinstance(module['functions'], list) and len(module['functions']) > 0:
1804
+ try:
1805
+ func = [func for func in module['functions'] if func['name'] == self.function_name][0]
1806
+ except Exception:
1807
+ raise exceptions.PlatformException(
1808
+ '400', 'function {} does not exist in package module'.format(self.function_name)
1809
+ )
1810
+ else:
1811
+ func = entities.PackageFunction(None, list(), list(), None).to_json()
1812
+ package_inputs = func['input']
1813
+ current_level = logging.root.level
1814
+ logging.root.setLevel('INFO')
1815
+ if not self.multithreading:
1816
+ kwargs = dict()
1817
+ progress = utilities.Progress()
1818
+
1819
+ func = getattr(package_runner, self.function_name)
1820
+ params = list(inspect.signature(func).parameters)
1821
+ if "progress" in params:
1822
+ kwargs['progress'] = progress
1823
+
1824
+ for package_input in package_inputs:
1825
+ kwargs[package_input['name']] = self.get_field(field_name=package_input['name'],
1826
+ field_type=package_input['type'],
1827
+ project=project, mock_json=self.mock_json)
1828
+
1829
+ results = getattr(package_runner, self.function_name)(**kwargs)
1830
+ else:
1831
+ pool = ThreadPoolExecutor(max_workers=self.concurrency)
1832
+ inputs = self.mock_json['inputs']
1833
+ results = list()
1834
+ jobs = list()
1835
+ for single_input in inputs:
1836
+ kwargs = dict()
1837
+ progress = utilities.Progress()
1838
+ kwargs['progress'] = progress
1839
+ for package_input in package_inputs:
1840
+ kwargs[package_input['name']] = self.get_field(field_name=package_input['name'],
1841
+ field_type=package_input['type'],
1842
+ project=project,
1843
+ mock_json=self.mock_json,
1844
+ mock_inputs=single_input)
1845
+ jobs.append(
1846
+ pool.submit(
1847
+ getattr(package_runner, self.function_name),
1848
+ **kwargs
1849
+ )
1850
+ )
1851
+ for job in jobs:
1852
+ results.append(job.result())
1853
+
1854
+ logging.root.level = current_level
1855
+ return results
1856
+
1857
+ @staticmethod
1858
+ def _is_entity(output_type: str):
1859
+ # output_type is a entity class starts with capital
1860
+ return hasattr(entities, output_type)
1861
+
1862
+ def _fetch_single_resource(self, value, output_type):
1863
+ if isinstance(value, dict):
1864
+ params = {'{}_id'.format(output_type.lower()): value['{}_id'.format(output_type.lower())]}
1865
+ else:
1866
+ params = {'{}_id'.format(output_type.lower()): value}
1867
+ return getattr(repositories, '{}s'.format(output_type))(self._client_api).get(**params)
1868
+
1869
+ def get_field(self, field_name, field_type, mock_json, project=None, mock_inputs=None):
1870
+ """
1871
+ Get field in mock json.
1872
+
1873
+ :param field_name: field name
1874
+ :param field_type: field type
1875
+ :param mock_json: mock json
1876
+ :param project: project
1877
+ :param mock_inputs: mock inputs
1878
+ :return:
1879
+ """
1880
+ if mock_inputs is None:
1881
+ mock_inputs = mock_json['inputs']
1882
+ filtered_mock_inputs = list(filter(lambda input_field: input_field['name'] == field_name, mock_inputs))
1883
+
1884
+ if len(filtered_mock_inputs) == 0:
1885
+ raise Exception('No entry for field {} found in mock'.format(field_name))
1886
+ if len(filtered_mock_inputs) > 1:
1887
+ raise Exception('Duplicate entries for field {} found in mock'.format(field_name))
1888
+
1889
+ if field_type not in list(entities.PackageInputType):
1890
+ raise exceptions.PlatformException('400', 'Unknown resource type for field {}'.format(field_name))
1891
+
1892
+ mock_input = filtered_mock_inputs[0]
1893
+ resource_id = mock_input['value']
1894
+
1895
+ if field_type.endswith('[]') and self._is_entity(output_type=field_type.replace('[]', '')):
1896
+ field_type = field_type.replace('[]', '')
1897
+ value = resource_id if isinstance(resource_id, list) else [resource_id]
1898
+ return [
1899
+ self._fetch_single_resource(value=val, output_type=field_type) for val in value
1900
+ ]
1901
+ elif self._is_entity(output_type=field_type):
1902
+ return self._fetch_single_resource(value=resource_id, output_type=field_type)
1903
+ else:
1904
+ return resource_id
1905
+
1906
+
1907
+ class PackageIO:
1908
+
1909
+ def __init__(self, cwd=None):
1910
+ if cwd is None:
1911
+ cwd = os.getcwd()
1912
+
1913
+ self.package_file_path = os.path.join(cwd, assets.paths.PACKAGE_FILENAME)
1914
+
1915
+ def read_json(self):
1916
+ file_path = self.package_file_path
1917
+
1918
+ with open(file_path, 'r') as fp:
1919
+ cfg = json.load(fp)
1920
+ return cfg
1921
+
1922
+ def get(self, key):
1923
+ """
1924
+ :param key:
1925
+ """
1926
+ cfg = self.read_json()
1927
+ return cfg[key]
1928
+
1929
+ def put(self, key, value):
1930
+ """
1931
+ :param key:
1932
+ :param value:
1933
+ """
1934
+ try:
1935
+ cfg = self.read_json()
1936
+ cfg[key] = value
1937
+ file_path = self.package_file_path
1938
+ with open(file_path, 'w') as fp:
1939
+ json.dump(cfg, fp, indent=2)
1940
+ except Exception:
1941
+ pass