dtlpy 1.118.15__py3-none-any.whl → 1.119.5__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.
dtlpy/entities/dpk.py CHANGED
@@ -1,10 +1,15 @@
1
1
  from collections import namedtuple
2
- from typing import List, Union
2
+ from typing import List, Union, Tuple
3
3
  import traceback
4
4
  import enum
5
+ import tempfile
6
+ import inspect
7
+ import os
8
+ import typing
5
9
 
6
- from .. import entities, repositories, exceptions
10
+ from .. import entities, repositories, exceptions, assets
7
11
  from ..services.api_client import ApiClient
12
+ from . import package_defaults
8
13
 
9
14
 
10
15
  class SlotType(str, enum.Enum):
@@ -29,6 +34,18 @@ DEFAULT_STOPS = {SlotType.ITEM_VIEWER: {"type": "itemViewer",
29
34
  }
30
35
  }
31
36
 
37
+ DEFAULT_RUNTIME = {
38
+ "podType": "regular-xs",
39
+ "concurrency": 1,
40
+ "runnerImage": "docker.io/dataloopai/dtlpy-agent:cpu.py3.10.opencv",
41
+ "autoscaler": {
42
+ "type": "rabbitmq",
43
+ "minReplicas": 0,
44
+ "maxReplicas": 2,
45
+ "queueLength": 100
46
+ }
47
+ }
48
+
32
49
 
33
50
  class Slot(entities.DlEntity):
34
51
  type: str = entities.DlProperty(location=['type'], _type=str)
@@ -468,3 +485,299 @@ class Dpk(entities.DlEntity):
468
485
  )
469
486
 
470
487
  return res
488
+
489
+ @staticmethod
490
+ def _parse_function_io(func) -> Tuple[List[dict], List[dict]]:
491
+ """
492
+ Parse function inputs and outputs from function signature and type hints.
493
+
494
+ This helper function extracts:
495
+ - Inputs: from function parameters (inspect.signature)
496
+ - Outputs: from return type hints (typing.get_type_hints)
497
+
498
+ :param func: The function to parse
499
+ :return: Tuple of (inputs, outputs) where each is a list of dicts with 'name' and 'type' keys
500
+ :rtype: Tuple[List[dict], List[dict]]
501
+ """
502
+ # ============================================
503
+ # PARSE INPUTS FROM FUNCTION SIGNATURE
504
+ # ============================================
505
+ # Get all parameter names from the function signature
506
+ params = list(inspect.signature(func).parameters)
507
+ inputs = []
508
+
509
+ # Create a mapping of known input type names (lowercase) to PackageInputType values
510
+ # This allows matching parameter names like 'item', 'items', 'annotation', etc. to their types
511
+ inputs_types = {i.name.lower(): i.value for i in list(entities.PackageInputType)}
512
+
513
+ # Process each parameter
514
+ for arg in params:
515
+ # Check if the parameter name matches a known PackageInputType (case-insensitive)
516
+ # For example: 'item' -> PackageInputType.ITEM, 'items' -> PackageInputType.ITEMS
517
+ if arg in inputs_types:
518
+ inpt_type = inputs_types[arg]
519
+ else:
520
+ # If parameter name doesn't match a known type, default to JSON
521
+ inpt_type = entities.PackageInputType.JSON
522
+
523
+ # Create input dict with name and type
524
+ inputs.append({
525
+ 'name': arg,
526
+ 'type': inpt_type
527
+ })
528
+
529
+ # ============================================
530
+ # PARSE OUTPUTS FROM TYPE HINTS
531
+ # ============================================
532
+ outputs = []
533
+ try:
534
+ # Get return type hint from function annotations
535
+ # Returns None if no return type is specified
536
+ hint_outputs = typing.get_type_hints(func).get('return', None)
537
+
538
+ if hint_outputs is not None:
539
+ # ============================================
540
+ # STEP 1: Extract output types from type hint
541
+ # ============================================
542
+ # Handle different return type patterns:
543
+ # - Tuple[Type1, Type2, ...] -> multiple outputs
544
+ # - List[Type] -> single output that's a list
545
+ # - Type -> single output
546
+ output_types = []
547
+
548
+ if hasattr(hint_outputs, '__origin__') and hint_outputs.__origin__ is tuple:
549
+ # Multiple returns: Tuple[Type1, Type2, ...]
550
+ # Extract all types from the tuple
551
+ output_types = list(hint_outputs.__args__) if hasattr(hint_outputs, '__args__') else []
552
+ elif hasattr(hint_outputs, '__args__'):
553
+ # Single generic type (e.g., List[Item], Dict[str, int])
554
+ # Extract the inner type from the generic
555
+ output_types = [hint_outputs.__args__[0]]
556
+ else:
557
+ # Single simple type (e.g., Item, str, int)
558
+ output_types = [hint_outputs]
559
+
560
+ # ============================================
561
+ # STEP 2: Process each output type
562
+ # ============================================
563
+ for idx, output_type in enumerate(output_types):
564
+ io_type = output_type
565
+ is_list = False
566
+
567
+ # ============================================
568
+ # STEP 2a: Check if it's a List type
569
+ # ============================================
570
+ # Handle List[Type] patterns (e.g., List[Item], List[Annotation])
571
+ if hasattr(io_type, '__origin__'):
572
+ origin = io_type.__origin__
573
+ origin_str = str(origin)
574
+
575
+ # Check if origin is list or typing.List
576
+ # This handles both 'list' and 'typing.List' cases
577
+ if origin is list or origin_str.startswith('typing.List') or origin_str.startswith('<class \'list\'>'):
578
+ is_list = True
579
+ # Extract inner type from List[InnerType]
580
+ # Example: List[Item] -> extract 'Item'
581
+ if hasattr(io_type, '__args__') and len(io_type.__args__) > 0:
582
+ io_type = io_type.__args__[0]
583
+
584
+ # ============================================
585
+ # STEP 2b: Handle special type cases
586
+ # ============================================
587
+ # Handle Enum types - convert to string name
588
+ if isinstance(io_type, enum.Enum):
589
+ io_type = io_type.name
590
+
591
+ # Handle nested generic types (only if not already extracted from List)
592
+ # Example: Optional[List[Item]] -> extract List[Item] first, then Item
593
+ if not is_list and hasattr(io_type, '__args__'):
594
+ io_type = io_type.__args__[0]
595
+
596
+ # ============================================
597
+ # STEP 2c: Extract type name as string
598
+ # ============================================
599
+ # Convert type to string representation
600
+ if isinstance(io_type, type):
601
+ # Direct type object (e.g., Item class) -> get class name
602
+ type_str = io_type.__name__
603
+ else:
604
+ # Type hint object -> convert to string
605
+ type_str = str(io_type)
606
+ # Extract class name from fully qualified name
607
+ # Example: "dtlpy.entities.item.Item" -> "Item"
608
+ if '.' in type_str:
609
+ type_str = type_str.rsplit('.', maxsplit=1)[-1]
610
+
611
+ # ============================================
612
+ # STEP 2d: Map type string to PackageInputType
613
+ # ============================================
614
+ # Map Python type names to PackageInputType enum values
615
+ # Special case: 'str' maps to STRING
616
+ if type_str == 'str':
617
+ mapped_type = entities.PackageInputType.STRING
618
+ else:
619
+ mapped_type = None
620
+
621
+ # If it's a list, try to find the array version first
622
+ # Example: Item -> ITEMS, Annotation -> ANNOTATIONS
623
+ if is_list:
624
+ array_type_name = f"{type_str.upper()}S" # Item -> ITEMS
625
+ for pkg_input_type in entities.PackageInputType:
626
+ if pkg_input_type.name.upper() == array_type_name:
627
+ mapped_type = pkg_input_type
628
+ break
629
+
630
+ # If not found or not a list, try the regular type
631
+ # Example: Item -> ITEM, Annotation -> ANNOTATION
632
+ if mapped_type is None:
633
+ for pkg_input_type in entities.PackageInputType:
634
+ if pkg_input_type.name.upper() == type_str.upper():
635
+ mapped_type = pkg_input_type
636
+ break
637
+
638
+ # Default to JSON if no match found
639
+ # This handles unknown types gracefully
640
+ if mapped_type is None:
641
+ mapped_type = entities.PackageInputType.JSON
642
+
643
+ # ============================================
644
+ # STEP 2e: Create output dict
645
+ # ============================================
646
+ # Name outputs: 'output' for single, 'output_0', 'output_1', etc. for multiple
647
+ output_name = 'output' if len(output_types) == 1 else f'output_{idx}'
648
+ outputs.append({
649
+ 'name': output_name,
650
+ 'type': mapped_type
651
+ })
652
+ except Exception:
653
+ # If type hints extraction fails (e.g., no type hints, invalid syntax),
654
+ # leave outputs empty - this is not a fatal error
655
+ pass
656
+
657
+ return inputs, outputs
658
+
659
+ @classmethod
660
+ def from_function(
661
+ cls,
662
+ func,
663
+ name: str = None,
664
+ version: str = '1.0.0',
665
+ description: str = None,
666
+ project: 'entities.Project' = None,
667
+ client_api: ApiClient = None,
668
+ scope: str = 'project',
669
+ runtime: dict = None,
670
+ execution_timeout: int = 3600,
671
+ ) -> 'entities.Dpk':
672
+ """
673
+ Build a DPK from a Python function (codebase + components). Does not publish or install.
674
+ Runtime is held on the service component (modules have no runtime). Use the runtime param to override defaults.
675
+
676
+ :param Callable func: Function to deploy
677
+ :param str name: DPK name (default: function __name__)
678
+ :param str version: DPK version (default: '1.0.0')
679
+ :param str description: DPK description
680
+ :param entities.Project project: Project (required)
681
+ :param ApiClient client_api: API client (required)
682
+ :param str scope: 'project' or 'organization'
683
+ :param dict runtime: podType, concurrency, runnerImage, autoscaler; default: DEFAULT_RUNTIME
684
+ :param int execution_timeout: Service timeout in seconds (default: 3600)
685
+ :return: Built DPK (with codebase); use Service.from_function to publish, install, and get the Service
686
+ :rtype: dtlpy.entities.Dpk
687
+ """
688
+ if client_api is None:
689
+ raise ValueError('client_api is required')
690
+ if project is None:
691
+ raise ValueError('project is required for codebase packing')
692
+
693
+ # Get function name if name not provided
694
+ if name is None:
695
+ name = func.__name__
696
+
697
+ # Resolve runtime: use copy to avoid mutating caller's dict or DEFAULT_RUNTIME
698
+ if runtime is None:
699
+ runtime = dict(DEFAULT_RUNTIME)
700
+ else:
701
+ runtime = dict(runtime)
702
+
703
+ # Create temporary directory for the function code
704
+ dpk_dir = tempfile.mkdtemp()
705
+
706
+ # Create main.py file with the function
707
+ main_file = os.path.join(dpk_dir, package_defaults.DEFAULT_PACKAGE_ENTRY_POINT)
708
+
709
+ # Read the partial main template (ServiceRunner class structure)
710
+ with open(assets.paths.PARTIAL_MAIN_FILEPATH, 'r') as f:
711
+ main_template = f.read()
712
+
713
+ # Extract function source code and adjust indentation
714
+ lines = inspect.getsourcelines(func)
715
+ tabs_diff = lines[0][0].count(' ') - 1
716
+ for line_index in range(len(lines[0])):
717
+ line_tabs = lines[0][line_index].count(' ') - tabs_diff
718
+ lines[0][line_index] = (' ' * line_tabs) + lines[0][line_index].strip() + '\n'
719
+
720
+ method_func_string = "".join(lines[0])
721
+
722
+ # Write main.py with ServiceRunner class and function
723
+ with open(main_file, 'w') as f:
724
+ f.write(f'{main_template}\n @staticmethod\n{method_func_string}')
725
+
726
+ # Parse function inputs and outputs from signature and type hints
727
+ inputs, outputs = cls._parse_function_io(func)
728
+
729
+ # Build module dict (no runtime on module/functions)
730
+ function_dict = {
731
+ 'name': func.__name__,
732
+ 'input': inputs,
733
+ 'output': outputs,
734
+ }
735
+ module_dict = {
736
+ 'name': package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
737
+ 'entryPoint': package_defaults.DEFAULT_PACKAGE_ENTRY_POINT,
738
+ 'className': package_defaults.DEFAULT_PACKAGE_CLASS_NAME,
739
+ 'functions': [function_dict],
740
+ }
741
+
742
+ # Service component (smart-search style): holds runtime, references module
743
+ service_name = f"{name}-service".replace("_", "-")
744
+ service_entry = {
745
+ "name": service_name,
746
+ "moduleName": package_defaults.DEFAULT_PACKAGE_MODULE_NAME,
747
+ "packageRevision": "latest",
748
+ "runtime": runtime,
749
+ "executionTimeout": execution_timeout,
750
+ }
751
+
752
+ components_dict = {
753
+ "modules": [module_dict],
754
+ "services": [service_entry],
755
+ }
756
+
757
+ # Create DPK entity
758
+ dpk_json = {
759
+ 'name': name,
760
+ 'version': version,
761
+ 'display_name': name,
762
+ 'description': description or f'DPK created from function {func.__name__}',
763
+ 'scope': scope,
764
+ 'components': components_dict,
765
+ 'context': {
766
+ 'project': project.id
767
+ }
768
+ }
769
+
770
+ dpk = cls.from_json(
771
+ _json=dpk_json,
772
+ client_api=client_api,
773
+ project=project
774
+ )
775
+
776
+ # Pack the temporary directory as codebase
777
+ dpk.codebase = project.codebases.pack(
778
+ directory=dpk_dir,
779
+ name=name,
780
+ extension='dpk'
781
+ )
782
+
783
+ return dpk
dtlpy/entities/model.py CHANGED
@@ -51,29 +51,6 @@ class PlotSample:
51
51
  return _json
52
52
 
53
53
 
54
- # class MatrixSample:
55
- # def __init__(self, figure, legend, x, y):
56
- # """
57
- # Create a single metric sample for Model
58
- #
59
- # :param figure: figure name identifier
60
- # :param legend: line name identifier
61
- # :param x: x value for the current sample
62
- # :param y: y value for the current sample
63
- # """
64
- # self.figure = figure
65
- # self.legend = legend
66
- # self.x = x
67
- # self.y = y
68
- #
69
- # def to_json(self) -> dict:
70
- # _json = {'figure': self.figure,
71
- # 'legend': self.legend,
72
- # 'data': {'x': self.x,
73
- # 'y': self.y}}
74
- # return _json
75
-
76
-
77
54
  @attr.s
78
55
  class Model(entities.BaseEntity):
79
56
  """
dtlpy/entities/service.py CHANGED
@@ -8,6 +8,7 @@ from urllib.parse import urlsplit
8
8
  import attr
9
9
  from .. import repositories, entities
10
10
  from ..services.api_client import ApiClient
11
+ from .dpk import Dpk
11
12
 
12
13
  logger = logging.getLogger(name='dtlpy')
13
14
 
@@ -359,6 +360,50 @@ class Service(entities.BaseEntity):
359
360
  inst.is_fetched = is_fetched
360
361
  return inst
361
362
 
363
+ @classmethod
364
+ def from_function(
365
+ cls,
366
+ func,
367
+ name: str = None,
368
+ version: str = '1.0.0',
369
+ description: str = None,
370
+ project: 'entities.Project' = None,
371
+ client_api: ApiClient = None,
372
+ scope: str = 'project',
373
+ runtime: dict = None,
374
+ execution_timeout: int = 3600,
375
+ ) -> 'Service':
376
+ """
377
+ Create a Service from a Python function: build DPK via Dpk.from_function, then publish, install, and return the Service.
378
+ """
379
+ built_dpk = Dpk.from_function(
380
+ func=func,
381
+ name=name,
382
+ version=version,
383
+ description=description,
384
+ project=project,
385
+ client_api=client_api,
386
+ scope=scope,
387
+ runtime=runtime,
388
+ execution_timeout=execution_timeout,
389
+ )
390
+ published_dpk = project.dpks.publish(dpk=built_dpk)
391
+ app = project.apps.install(dpk=published_dpk, app_name=published_dpk.display_name)
392
+ service_name = f"{built_dpk.name}-service".replace("_", "-")
393
+ filters = entities.Filters(
394
+ resource=entities.FiltersResource.SERVICE,
395
+ field='name',
396
+ values=service_name,
397
+ use_defaults=False
398
+ )
399
+ services = project.services.list(filters=filters)
400
+ if services.items_count == 0:
401
+ raise RuntimeError(
402
+ "No service found with name %s for installed app (app id=%s). "
403
+ "The app was installed but the service may not be listed yet." % (service_name, app.id)
404
+ )
405
+ return services.items[0]
406
+
362
407
  ############
363
408
  # Entities #
364
409
  ############
@@ -397,7 +442,7 @@ class Service(entities.BaseEntity):
397
442
  dpk_id=dpk_id,
398
443
  version=dpk_version)
399
444
 
400
- assert isinstance(self._package, entities.Dpk)
445
+ assert isinstance(self._package, Dpk)
401
446
  except:
402
447
  self._package = repositories.Packages(client_api=self._client_api).get(package_id=self.package_id,
403
448
  fetch=None,
@@ -653,7 +653,8 @@ class Annotations:
653
653
  not a.get('clean', False) and a.get("metadata", {}).get('system', {}).get('objectId',
654
654
  None) ==
655
655
  annotation.get("metadata", {}).get('system', {}).get('objectId', None) and a['label'] ==
656
- annotation['label']]
656
+ annotation['label']
657
+ and a['type'] == annotation['type']]
657
658
  if len(to_merge) == 0:
658
659
  # no annotation to merge with
659
660
  continue
@@ -677,7 +678,8 @@ class Annotations:
677
678
  to_merge = [a for a in exist_annotations if
678
679
  a.object_id == ann.get("metadata", {}).get('system', {}).get('objectId',
679
680
  None) and a.label == ann[
680
- 'label']]
681
+ 'label']
682
+ and a.type == ann['type']]
681
683
  if len(to_merge) == 0:
682
684
  # no annotation to merge with
683
685
  continue
@@ -11,7 +11,7 @@ class Apps:
11
11
  def __init__(self, client_api: ApiClient, project: entities.Project = None, project_id: str = None):
12
12
  self._client_api = client_api
13
13
  self._project = project
14
- self._project_id = project_id
14
+ self._project_id = project_id
15
15
  self._commands = None
16
16
 
17
17
  @property
@@ -358,7 +358,7 @@ class Computes:
358
358
  project_id = project.id
359
359
  compute = self.create(
360
360
  config['config']['name'],
361
- ComputeContext([], org_id, project_id),
361
+ ComputeContext(labels=[], org=org_id, project=project_id),
362
362
  [],
363
363
  cluster,
364
364
  ComputeType.KUBERNETES,
@@ -223,7 +223,7 @@ class FeatureSets:
223
223
 
224
224
  @_api_reference.add(path='/features/sets', method='post')
225
225
  def create(
226
- self, name: str, size: int, set_type: str, entity_type: entities.FeatureEntityType, project_id: str = None, model_id: set = None, org_id: str = None
226
+ self, name: str, size: int, set_type: str, entity_type: entities.FeatureEntityType, project_id: str = None, model_id: str = None, org_id: str = None
227
227
  ):
228
228
  """
229
229
  Create a new Feature Set