Ayon-Python-Api 0.1.7__tar.gz → 0.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {ayon-python-api-0.1.7/ayon_python_api.egg-info → ayon-python-api-0.1.9}/PKG-INFO +1 -1
  2. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/__init__.py +25 -0
  3. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/exceptions.py +4 -0
  4. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/server.py +220 -87
  5. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/server_api.py +172 -0
  6. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/utils.py +76 -17
  7. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/version.py +1 -1
  8. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9/ayon_python_api.egg-info}/PKG-INFO +1 -1
  9. ayon-python-api-0.1.9/ayon_python_api.egg-info/dependency_links.txt +1 -0
  10. ayon-python-api-0.1.9/ayon_python_api.egg-info/requires.txt +4 -0
  11. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/pyproject.toml +3 -2
  12. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/setup.py +2 -3
  13. ayon-python-api-0.1.7/ayon_python_api.egg-info/dependency_links.txt +0 -1
  14. ayon-python-api-0.1.7/ayon_python_api.egg-info/requires.txt +0 -2
  15. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/LICENSE +0 -0
  16. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/README.md +0 -0
  17. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/constants.py +0 -0
  18. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/entity_hub.py +0 -0
  19. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/events.py +0 -0
  20. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/graphql.py +0 -0
  21. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/graphql_queries.py +0 -0
  22. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/operations.py +0 -0
  23. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_api/thumbnails.py +0 -0
  24. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  25. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/ayon_python_api.egg-info/top_level.txt +0 -0
  26. {ayon-python-api-0.1.7 → ayon-python-api-0.1.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,5 +1,6 @@
1
1
  from .utils import (
2
2
  TransferProgress,
3
+ slugify_string,
3
4
  )
4
5
  from .server import (
5
6
  ServerAPIBase,
@@ -7,7 +8,14 @@ from .server import (
7
8
 
8
9
  from .server_api import (
9
10
  ServerAPI,
11
+ ServiceContext,
10
12
 
13
+ init_service,
14
+
15
+ is_connection_created,
16
+ close_connection,
17
+ change_token,
18
+ set_environments,
11
19
  get_server_api_connection,
12
20
 
13
21
  get_base_url,
@@ -25,6 +33,11 @@ from .server_api import (
25
33
  patch,
26
34
  delete,
27
35
 
36
+ get_event,
37
+ dispatch_event,
38
+ update_event,
39
+ enroll_event_job,
40
+
28
41
  download_file,
29
42
  upload_file,
30
43
 
@@ -99,11 +112,18 @@ from .server_api import (
99
112
 
100
113
  __all__ = (
101
114
  "TransferProgress",
115
+ "slugify_string",
102
116
 
103
117
  "ServerAPIBase",
104
118
 
105
119
  "ServerAPI",
120
+ "ServiceContext",
121
+ "init_service",
106
122
 
123
+ "is_connection_created",
124
+ "close_connection",
125
+ "change_token",
126
+ "set_environments",
107
127
  "get_server_api_connection",
108
128
 
109
129
  "get_base_url",
@@ -121,6 +141,11 @@ __all__ = (
121
141
  "patch",
122
142
  "delete",
123
143
 
144
+ "get_event",
145
+ "dispatch_event",
146
+ "update_event",
147
+ "enroll_event_job",
148
+
124
149
  "download_file",
125
150
  "upload_file",
126
151
 
@@ -90,4 +90,8 @@ class FolderNotFound(MissingEntityError):
90
90
 
91
91
 
92
92
  class FailedOperations(Exception):
93
+ pass
94
+
95
+
96
+ class FailedServiceInit(Exception):
93
97
  pass
@@ -75,13 +75,32 @@ class RequestTypes:
75
75
  class RestApiResponse(object):
76
76
  """API Response."""
77
77
 
78
- def __init__(self, status_code, data=None, content=None, headers=None):
79
- if data is None:
80
- data = {}
78
+ def __init__(self, response, data=None):
79
+ if response is None:
80
+ status_code = 500
81
+ else:
82
+ status_code = response.status_code
83
+ self._response = response
81
84
  self.status = status_code
82
- self.data = data
83
- self.content = content
84
- self.headers = headers or {}
85
+ self._data = data
86
+
87
+ @property
88
+ def orig_response(self):
89
+ return self._response
90
+
91
+ @property
92
+ def headers(self):
93
+ return self._response.headers
94
+
95
+ @property
96
+ def data(self):
97
+ if self._data is None:
98
+ self._data = self._response.json()
99
+ return self._data
100
+
101
+ @property
102
+ def content(self):
103
+ return self._response.content
85
104
 
86
105
  @property
87
106
  def content_type(self):
@@ -95,6 +114,12 @@ class RestApiResponse(object):
95
114
  def status_code(self):
96
115
  return self.status
97
116
 
117
+ def raise_for_status(self):
118
+ self._response.raise_for_status()
119
+
120
+ def __enter__(self, *args, **kwargs):
121
+ return self._response.__enter__(*args, **kwargs)
122
+
98
123
  def __contains__(self, key):
99
124
  return key in self.data
100
125
 
@@ -448,26 +473,22 @@ class ServerAPIBase(object):
448
473
 
449
474
  except ConnectionRefusedError:
450
475
  new_response = RestApiResponse(
451
- 500,
476
+ None,
452
477
  {"detail": "Unable to connect the server. Connection refused"}
453
478
  )
454
479
  except requests.exceptions.ConnectionError:
455
480
  new_response = RestApiResponse(
456
- 500,
481
+ None,
457
482
  {"detail": "Unable to connect the server. Connection error"}
458
483
  )
459
484
  else:
460
485
  content_type = response.headers.get("Content-Type")
461
486
  if content_type == "application/json":
462
487
  try:
463
- new_response = RestApiResponse(
464
- response.status_code,
465
- response.json(),
466
- headers=response.headers,
467
- )
488
+ new_response = RestApiResponse(response)
468
489
  except JSONDecodeError:
469
490
  new_response = RestApiResponse(
470
- 500,
491
+ None,
471
492
  {
472
493
  "detail": "The response is not a JSON: {}".format(
473
494
  response.text)
@@ -475,17 +496,10 @@ class ServerAPIBase(object):
475
496
  )
476
497
 
477
498
  elif content_type in ("image/jpeg", "image/png"):
478
- new_response = RestApiResponse(
479
- response.status_code,
480
- headers=response.headers,
481
- content=response.content
482
- )
499
+ new_response = RestApiResponse(response)
483
500
 
484
501
  else:
485
- new_response = RestApiResponse(
486
- response.status_code,
487
- headers=response.headers,
488
- )
502
+ new_response = RestApiResponse(response)
489
503
 
490
504
  self.log.debug("Response {}".format(str(new_response)))
491
505
  return new_response
@@ -555,6 +569,119 @@ class ServerAPIBase(object):
555
569
  def delete(self, entrypoint, **kwargs):
556
570
  return self.raw_delete(entrypoint, params=kwargs)
557
571
 
572
+ def get_event(self, event_id):
573
+ """Receive full event data by id.
574
+
575
+ Events received using event server do not contain full information. To
576
+ get the full event information is required to receive it explicitly.
577
+
578
+ Args:
579
+ event_id (str): Id of event.
580
+
581
+ Returns:
582
+ Dict[str, Any]: Full event data.
583
+ """
584
+
585
+ response = self.get("events/{}".format(event_id))
586
+ response.raise_for_status()
587
+ return response.data
588
+
589
+ def update_event(
590
+ self,
591
+ event_id,
592
+ sender=None,
593
+ project_name=None,
594
+ status=None,
595
+ description=None,
596
+ summary=None,
597
+ payload=None
598
+ ):
599
+ kwargs = {}
600
+ for key, value in (
601
+ ("sender", sender),
602
+ ("projectName", project_name),
603
+ ("status", status),
604
+ ("description", description),
605
+ ("summary", summary),
606
+ ("payload", payload),
607
+ ):
608
+ if value is not None:
609
+ kwargs[key] = value
610
+ response = self.patch(
611
+ "events/{}".format(event_id),
612
+ **kwargs
613
+ )
614
+ response.raise_for_status()
615
+
616
+ def dispatch_event(
617
+ self,
618
+ topic,
619
+ sender=None,
620
+ event_hash=None,
621
+ project_name=None,
622
+ username=None,
623
+ dependencies=None,
624
+ description=None,
625
+ summary=None,
626
+ payload=None,
627
+ finished=True,
628
+ store=True,
629
+ ):
630
+ if summary is None:
631
+ summary = {}
632
+ if payload is None:
633
+ payload = {}
634
+ event_data = {
635
+ "topic": topic,
636
+ "sender": sender,
637
+ "hash": event_hash,
638
+ "project": project_name,
639
+ "user": username,
640
+ "dependencies": dependencies,
641
+ "description": description,
642
+ "summary": summary,
643
+ "payload": payload,
644
+ "finished": finished,
645
+ "store": store,
646
+ }
647
+ if self.post("events", **event_data):
648
+ self.log.debug("Dispatched event {}".format(topic))
649
+ return True
650
+ self.log.error("Unable to dispatch event {}".format(topic))
651
+ return False
652
+
653
+ def enroll_event_job(
654
+ self,
655
+ source_topic,
656
+ target_topic,
657
+ sender,
658
+ description=None,
659
+ sequential=None
660
+ ):
661
+ """Enroll job based on events.
662
+
663
+ Args:
664
+ source_topic (str): Topic
665
+ """
666
+
667
+ kwargs = {
668
+ "sourceTopic": source_topic,
669
+ "targetTopic": target_topic,
670
+ "sender": sender,
671
+ }
672
+ if sequential is not None:
673
+ kwargs["sequential"] = sequential
674
+ if description is not None:
675
+ kwargs["description"] = description
676
+ response = self.post("enroll", **kwargs)
677
+ if response.status_code == 204:
678
+ return None
679
+ elif response.status_code >= 400:
680
+ self.log.error(response.text)
681
+ return None
682
+
683
+ return response.data
684
+
558
685
  def _download_file(self, url, filepath, chunk_size, progress):
559
686
  dst_directory = os.path.dirname(filepath)
560
687
  if not os.path.exists(dst_directory):
@@ -573,7 +700,7 @@ class ServerAPIBase(object):
573
700
  progress.set_content_size(response.headers["Content-length"])
574
701
  for chunk in response.iter_content(chunk_size=chunk_size):
575
702
  f_stream.write(chunk)
576
- progress.add_downloaded_chunk(len(chunk))
703
+ progress.add_transferred_chunk(len(chunk))
577
704
 
578
705
  def download_file(self, endpoint, filepath, chunk_size=None, progress=None):
579
706
  """Download file from AYON server.
@@ -618,7 +745,7 @@ class ServerAPIBase(object):
618
745
  raise
619
746
 
620
747
  finally:
621
- progress.set_done()
748
+ progress.set_transfer_done()
622
749
 
623
750
  def _upload_file(self, url, filepath, progress):
624
751
  kwargs = {}
@@ -635,7 +762,7 @@ class ServerAPIBase(object):
635
762
  progress.set_content_size(size)
636
763
  response = post_func(url, data=stream, **kwargs)
637
764
  response.raise_for_status()
638
- progress.set_downloaded_size(size)
765
+ progress.set_transferred_size(size)
639
766
 
640
767
  def upload_file(self, endpoint, filepath, progress=None):
641
768
  """Upload file to server.
@@ -673,7 +800,7 @@ class ServerAPIBase(object):
673
800
  raise
674
801
 
675
802
  finally:
676
- progress.set_done()
803
+ progress.set_transfer_done()
677
804
 
678
805
  def trigger_server_restart(self):
679
806
  result = self.post("system/restart")
@@ -889,8 +1016,7 @@ class ServerAPIBase(object):
889
1016
  if details:
890
1017
  endpoint += "?details=1"
891
1018
  response = self.get(endpoint)
892
- if response.status != 200:
893
- raise ServerError(response.text)
1019
+ response.raise_for_status()
894
1020
  return response.data
895
1021
 
896
1022
  def download_addon_private_file(
@@ -1106,8 +1232,7 @@ class ServerAPIBase(object):
1106
1232
  result = self.get(
1107
1233
  "addons/{}/{}/settings".format(addon_name, addon_version)
1108
1234
  )
1109
- if result.status != 200:
1110
- raise ServerError(result.text)
1235
+ result.raise_for_status()
1111
1236
  return result.data
1112
1237
 
1113
1238
  def get_addon_project_settings(
@@ -1118,8 +1243,7 @@ class ServerAPIBase(object):
1118
1243
  addon_name, addon_version, project_name
1119
1244
  )
1120
1245
  )
1121
- if result.status != 200:
1122
- raise ServerError(result.text)
1246
+ result.raise_for_status()
1123
1247
  return result.data
1124
1248
 
1125
1249
  def get_addon_settings(self, addon_name, addon_version, project_name=None):
@@ -1160,7 +1284,7 @@ class ServerAPIBase(object):
1160
1284
  returned if 'None' is passed.
1161
1285
 
1162
1286
  Returns:
1163
- List[Dict[str, Any]]: List of available projects.
1287
+ Generator[Dict[str, Any]]: Available projects.
1164
1288
  """
1165
1289
 
1166
1290
  for project_name in self.get_project_names(active, library):
@@ -1223,8 +1347,7 @@ class ServerAPIBase(object):
1223
1347
  ]))
1224
1348
 
1225
1349
  response = self.get("projects{}".format(query), **query_keys)
1226
- # TODO check status
1227
- response.status
1350
+ response.raise_for_status()
1228
1351
  data = response.data
1229
1352
  project_names = []
1230
1353
  if data:
@@ -1248,7 +1371,7 @@ class ServerAPIBase(object):
1248
1371
  on entity will have 'None' value.
1249
1372
 
1250
1373
  Returns:
1251
- List[Dict[str, Any]]: List of queried projects.
1374
+ Generator[Dict[str, Any]]: Queried projects.
1252
1375
  """
1253
1376
 
1254
1377
  if fields is None:
@@ -1357,11 +1480,11 @@ class ServerAPIBase(object):
1357
1480
  if 'None' is passed.
1358
1481
 
1359
1482
  Returns:
1360
- Iterable[dict[str, Any]]: Queried folder entities.
1483
+ Generator[dict[str, Any]]: Queried folder entities.
1361
1484
  """
1362
1485
 
1363
1486
  if not project_name:
1364
- return []
1487
+ return
1365
1488
 
1366
1489
  filters = {
1367
1490
  "projectName": project_name
@@ -1369,25 +1492,25 @@ class ServerAPIBase(object):
1369
1492
  if folder_ids is not None:
1370
1493
  folder_ids = set(folder_ids)
1371
1494
  if not folder_ids:
1372
- return []
1495
+ return
1373
1496
  filters["folderIds"] = list(folder_ids)
1374
1497
 
1375
1498
  if folder_paths is not None:
1376
1499
  folder_paths = set(folder_paths)
1377
1500
  if not folder_paths:
1378
- return []
1501
+ return
1379
1502
  filters["folderPaths"] = list(folder_paths)
1380
1503
 
1381
1504
  if folder_names is not None:
1382
1505
  folder_names = set(folder_names)
1383
1506
  if not folder_names:
1384
- return []
1507
+ return
1385
1508
  filters["folderNames"] = list(folder_names)
1386
1509
 
1387
1510
  if parent_ids is not None:
1388
1511
  parent_ids = set(parent_ids)
1389
1512
  if not parent_ids:
1390
- return []
1513
+ return
1391
1514
  if None in parent_ids:
1392
1515
  # Replace 'None' with '"root"' which is used during GraphQl
1393
1516
  # query for parent ids filter for folders without folder
@@ -1448,7 +1571,7 @@ class ServerAPIBase(object):
1448
1571
  own_attributes=False
1449
1572
  ):
1450
1573
  if not project_name:
1451
- return []
1574
+ return
1452
1575
 
1453
1576
  filters = {
1454
1577
  "projectName": project_name
@@ -1457,25 +1580,25 @@ class ServerAPIBase(object):
1457
1580
  if task_ids is not None:
1458
1581
  task_ids = set(task_ids)
1459
1582
  if not task_ids:
1460
- return []
1583
+ return
1461
1584
  filters["taskIds"] = list(task_ids)
1462
1585
 
1463
1586
  if task_names is not None:
1464
1587
  task_names = set(task_names)
1465
1588
  if not task_names:
1466
- return []
1589
+ return
1467
1590
  filters["taskNames"] = list(task_names)
1468
1591
 
1469
1592
  if task_types is not None:
1470
1593
  task_types = set(task_types)
1471
1594
  if not task_types:
1472
- return []
1595
+ return
1473
1596
  filters["taskTypes"] = list(task_types)
1474
1597
 
1475
1598
  if folder_ids is not None:
1476
1599
  folder_ids = set(folder_ids)
1477
1600
  if not folder_ids:
1478
- return []
1601
+ return
1479
1602
  filters["folderIds"] = list(folder_ids)
1480
1603
 
1481
1604
  if not fields:
@@ -1633,6 +1756,20 @@ class ServerAPIBase(object):
1633
1756
  for folder in folders
1634
1757
  }
1635
1758
 
1759
+ def _filter_subset(
1760
+ self, project_name, subset, active, own_attributes, use_rest
1761
+ ):
1762
+ if active is not None and subset["active"] is not active:
1763
+ return None
1764
+
1765
+ if use_rest:
1766
+ subset = self.get_rest_subset(project_name, subset["id"])
1767
+
1768
+ if own_attributes:
1769
+ fill_own_attribs(subset)
1770
+
1771
+ return subset
1772
+
1636
1773
  def get_subsets(
1637
1774
  self,
1638
1775
  project_name,
@@ -1645,24 +1782,24 @@ class ServerAPIBase(object):
1645
1782
  own_attributes=False
1646
1783
  ):
1647
1784
  if not project_name:
1648
- return []
1785
+ return
1649
1786
 
1650
1787
  if subset_ids is not None:
1651
1788
  subset_ids = set(subset_ids)
1652
1789
  if not subset_ids:
1653
- return []
1790
+ return
1654
1791
 
1655
1792
  filter_subset_names = None
1656
1793
  if subset_names is not None:
1657
1794
  filter_subset_names = set(subset_names)
1658
1795
  if not filter_subset_names:
1659
- return []
1796
+ return
1660
1797
 
1661
1798
  filter_folder_ids = None
1662
1799
  if folder_ids is not None:
1663
1800
  filter_folder_ids = set(folder_ids)
1664
1801
  if not filter_folder_ids:
1665
- return []
1802
+ return
1666
1803
 
1667
1804
  # This will disable 'folder_ids' and 'subset_names' filters
1668
1805
  # - maybe could be enhanced in future?
@@ -1676,7 +1813,7 @@ class ServerAPIBase(object):
1676
1813
  filter_subset_names |= set(names)
1677
1814
 
1678
1815
  if not filter_subset_names or not filter_folder_ids:
1679
- return []
1816
+ return
1680
1817
 
1681
1818
  # Convert fields and add minimum required fields
1682
1819
  if fields:
@@ -1720,35 +1857,30 @@ class ServerAPIBase(object):
1720
1857
  parsed_data = query.query(self)
1721
1858
 
1722
1859
  subsets = parsed_data.get("project", {}).get("subsets", [])
1723
- if active is not None or own_attributes:
1724
- _subsets = []
1725
- for subset in subsets:
1726
- if active is not None and subset["active"] is not active:
1727
- continue
1728
-
1729
- if use_rest:
1730
- subset = self.get_rest_subset(project_name, subset["id"])
1731
-
1732
- if own_attributes:
1733
- fill_own_attribs(subset)
1734
- _subsets.append(subset)
1735
- subsets = _subsets
1736
-
1737
1860
  # Filter subsets by 'names_by_folder_ids'
1738
1861
  if names_by_folder_ids:
1739
1862
  subsets_by_folder_id = collections.defaultdict(list)
1740
1863
  for subset in subsets:
1741
- folder_id = subset["folderId"]
1742
- subsets_by_folder_id[folder_id].append(subset)
1864
+ filtered_subset = self._filter_subset(
1865
+ project_name, subset, active, own_attributes, use_rest
1866
+ )
1867
+ if filtered_subset is not None:
1868
+ folder_id = filtered_subset["folderId"]
1869
+ subsets_by_folder_id[folder_id].append(filtered_subset)
1743
1870
 
1744
- filtered_subsets = []
1745
1871
  for folder_id, names in names_by_folder_ids.items():
1746
1872
  for folder_subset in subsets_by_folder_id[folder_id]:
1747
1873
  if folder_subset["name"] in names:
1748
- filtered_subsets.append(subset)
1749
- subsets = filtered_subsets
1874
+ yield folder_subset
1875
+
1876
+ else:
1877
+ for subset in subsets:
1878
+ filtered_subset = self._filter_subset(
1879
+ project_name, subset, active, own_attributes, use_rest
1880
+ )
1881
+ if filtered_subset is not None:
1882
+ yield filtered_subset
1750
1883
 
1751
- return list(subsets)
1752
1884
 
1753
1885
  def get_subset_by_id(
1754
1886
  self,
@@ -1845,7 +1977,7 @@ class ServerAPIBase(object):
1845
1977
  if 'None' is passed.
1846
1978
 
1847
1979
  Returns:
1848
- List[Dict[str, Any]]: Queried version entities.
1980
+ Generator[Dict[str, Any]]: Queried version entities.
1849
1981
  """
1850
1982
 
1851
1983
  if not fields:
@@ -1872,24 +2004,24 @@ class ServerAPIBase(object):
1872
2004
  if version_ids is not None:
1873
2005
  version_ids = set(version_ids)
1874
2006
  if not version_ids:
1875
- return []
2007
+ return
1876
2008
  filters["versionIds"] = list(version_ids)
1877
2009
 
1878
2010
  if subset_ids is not None:
1879
2011
  subset_ids = set(subset_ids)
1880
2012
  if not subset_ids:
1881
- return []
2013
+ return
1882
2014
  filters["subsetIds"] = list(subset_ids)
1883
2015
 
1884
2016
  # TODO versions can't be used as fitler at this moment!
1885
2017
  if versions is not None:
1886
2018
  versions = set(versions)
1887
2019
  if not versions:
1888
- return []
2020
+ return
1889
2021
  filters["versions"] = list(versions)
1890
2022
 
1891
2023
  if not hero and not standard:
1892
- return []
2024
+ return
1893
2025
 
1894
2026
  queries = []
1895
2027
  # Add filters based on 'hero' and 'standard'
@@ -2158,7 +2290,7 @@ class ServerAPIBase(object):
2158
2290
  passed.
2159
2291
 
2160
2292
  Returns:
2161
- List[Dict[str, Any]]: Queried representation entities.
2293
+ Generator[Dict[str, Any]]: Queried representation entities.
2162
2294
  """
2163
2295
 
2164
2296
  if not fields:
@@ -2183,7 +2315,7 @@ class ServerAPIBase(object):
2183
2315
  if representation_ids is not None:
2184
2316
  representation_ids = set(representation_ids)
2185
2317
  if not representation_ids:
2186
- return []
2318
+ return
2187
2319
  filters["representationIds"] = list(representation_ids)
2188
2320
 
2189
2321
  version_ids_filter = None
@@ -2196,18 +2328,18 @@ class ServerAPIBase(object):
2196
2328
  representaion_names_filter |= set(names)
2197
2329
 
2198
2330
  if not version_ids_filter or not representaion_names_filter:
2199
- return []
2331
+ return
2200
2332
 
2201
2333
  else:
2202
2334
  if representation_names is not None:
2203
2335
  representaion_names_filter = set(representation_names)
2204
2336
  if not representaion_names_filter:
2205
- return []
2337
+ return
2206
2338
 
2207
2339
  if version_ids is not None:
2208
2340
  version_ids_filter = set(version_ids)
2209
2341
  if not version_ids_filter:
2210
- return []
2342
+ return
2211
2343
 
2212
2344
  if version_ids_filter:
2213
2345
  filters["versionIds"] = list(version_ids_filter)
@@ -2332,19 +2464,19 @@ class ServerAPIBase(object):
2332
2464
  if task_ids is not None:
2333
2465
  task_ids = set(task_ids)
2334
2466
  if not task_ids:
2335
- return []
2467
+ return
2336
2468
  filters["taskIds"] = list(task_ids)
2337
2469
 
2338
2470
  if paths is not None:
2339
2471
  paths = set(paths)
2340
2472
  if not paths:
2341
- return []
2473
+ return
2342
2474
  filters["paths"] = list(paths)
2343
2475
 
2344
2476
  if workfile_ids is not None:
2345
2477
  workfile_ids = set(workfile_ids)
2346
2478
  if not workfile_ids:
2347
- return []
2479
+ return
2348
2480
  filters["workfileIds"] = list(workfile_ids)
2349
2481
 
2350
2482
  if not fields:
@@ -2722,4 +2854,5 @@ class ServerAPIBase(object):
2722
2854
  operation_id,
2723
2855
  json.dumps(body_by_id[operation_id], indent=4),
2724
2856
  op_result["error"],
2725
- ))
2857
+ ))
2858
+
@@ -1,9 +1,12 @@
1
1
  import os
2
+ import socket
3
+
2
4
  from .constants import (
3
5
  SERVER_URL_ENV_KEY,
4
6
  SERVER_TOKEN_ENV_KEY,
5
7
  )
6
8
  from .server import ServerAPIBase
9
+ from .exceptions import FailedServiceInit
7
10
 
8
11
 
9
12
  class ServerAPI(ServerAPIBase):
@@ -65,6 +68,27 @@ class GlobalContext:
65
68
 
66
69
  _connection = None
67
70
 
71
+ @classmethod
72
+ def is_connection_created(cls):
73
+ return cls._connection is not None
74
+
75
+ @classmethod
76
+ def change_token(cls, url, token):
77
+ ServerAPI.set_environments(url, token)
78
+ if cls._connection is None:
79
+ return
80
+
81
+ if cls._connection.get_base_url() == url:
82
+ cls._connection.set_token(token)
83
+ else:
84
+ cls.close_connection()
85
+
86
+ @classmethod
87
+ def close_connection(cls):
88
+ if cls._connection is not None:
89
+ cls._connection.close_session()
90
+ cls._connection = None
91
+
68
92
  @classmethod
69
93
  def get_server_api_connection(cls):
70
94
  if cls._connection is None:
@@ -72,6 +96,134 @@ class GlobalContext:
72
96
  return cls._connection
73
97
 
74
98
 
99
+ class ServiceContext:
100
+ """Helper for services running under server.
101
+
102
+ When service is running from server the process receives information about
103
+ connection from environment variables. This class helps to initialize the
104
+ values without knowing environment variables (that may change over time).
105
+
106
+ All what must be done is to call 'init_service' function/method. The
107
+ arguments are for cases when the service is running in specific environment
108
+ and their values are e.g. loaded from private file or for testing purposes.
109
+ """
110
+
111
+ token = None
112
+ server_url = None
113
+ addon_name = None
114
+ addon_version = None
115
+ service_name = None
116
+
117
+ @staticmethod
118
+ def get_value_from_envs(env_keys, value=None):
119
+ if value:
120
+ return value
121
+
122
+ for env_key in env_keys:
123
+ value = os.environ.get(env_key)
124
+ if value:
125
+ break
126
+ return value
127
+
128
+ @classmethod
129
+ def init_service(
130
+ cls,
131
+ token=None,
132
+ server_url=None,
133
+ addon_name=None,
134
+ addon_version=None,
135
+ service_name=None,
136
+ connect=True
137
+ ):
138
+ token = cls.get_value_from_envs(
139
+ ("AY_API_KEY", "AYON_TOKEN"),
140
+ token
141
+ )
142
+ server_url = cls.get_value_from_envs(
143
+ ("AY_SERVER_URL", "AYON_SERVER_URL"),
144
+ server_url
145
+ )
146
+ if not server_url:
147
+ raise FailedServiceInit("URL to server is not set")
148
+
149
+ if not token:
150
+ raise FailedServiceInit(
151
+ "Token to server {} is not set".format(server_url)
152
+ )
153
+
154
+ addon_name = cls.get_value_from_envs(
155
+ ("AY_ADDON_NAME", "AYON_ADDON_NAME"),
156
+ addon_name
157
+ )
158
+ addon_version = cls.get_value_from_envs(
159
+ ("AY_ADDON_VERSION", "AYON_ADDON_VERSION"),
160
+ addon_version
161
+ )
162
+ service_name = cls.get_value_from_envs(
163
+ ("AY_SERVICE_NAME", "AYON_SERVICE_NAME"),
164
+ service_name
165
+ )
166
+
167
+ cls.token = token
168
+ cls.server_url = server_url
169
+ cls.addon_name = addon_name
170
+ cls.addon_version = addon_version
171
+ cls.service_name = service_name or socket.gethostname()
172
+
173
+ # Make sure required environments for ServerAPI are set
174
+ ServerAPI.set_environments(cls.server_url, cls.token)
175
+
176
+ if connect:
177
+ print("Connecting to server \"{}\"".format(server_url))
178
+ con = GlobalContext.get_server_api_connection()
179
+ user = con.get_user()
180
+ print("Logged in as user \"{}\"".format(user["name"]))
181
+
182
+
183
+ def init_service(*args, **kwargs):
184
+ ServiceContext.init_service(*args, **kwargs)
185
+
186
+
187
+ def is_connection_created():
188
+ """Is global connection created.
189
+
190
+ Returns:
191
+ bool: True if connection was connected.
192
+ """
193
+
194
+ return GlobalContext.is_connection_created()
195
+
196
+
197
+ def close_connection():
198
+ """Close global connection if is connected."""
199
+
200
+ GlobalContext.close_connection()
201
+
202
+
203
+ def change_token(url, token):
204
+ """Change connection token for url.
205
+
206
+ This function can be also used to change url.
207
+
208
+ Args:
209
+ url (str): Server url.
210
+ token (str): API key token.
211
+ """
212
+
213
+ GlobalContext.change_token(url, token)
214
+
215
+
216
+ def set_environments(url, token):
217
+ """Set global environments for global connection.
218
+
219
+ Args:
220
+ url (Union[str, None]): Url to server or None to unset environments.
221
+ token (Union[str, None]): API key token to be used for connection.
222
+ """
223
+
224
+ ServerAPI.set_environments(url, token)
225
+
226
+
75
227
  def get_server_api_connection():
76
228
  """Access to global scope object of ServerAPI.
77
229
 
@@ -145,6 +297,26 @@ def delete(*args, **kwargs):
145
297
  return con.delete(*args, **kwargs)
146
298
 
147
299
 
300
+ def get_event(*args, **kwargs):
301
+ con = get_server_api_connection()
302
+ return con.get_event(*args, **kwargs)
303
+
304
+
305
+ def dispatch_event(*args, **kwargs):
306
+ con = get_server_api_connection()
307
+ return con.dispatch_event(*args, **kwargs)
308
+
309
+
310
+ def update_event(*args, **kwargs):
311
+ con = get_server_api_connection()
312
+ return con.update_event(*args, **kwargs)
313
+
314
+
315
+ def enroll_event_job(*args, **kwargs):
316
+ con = get_server_api_connection()
317
+ return con.enroll_event_job(*args, **kwargs)
318
+
319
+
148
320
  def download_file(*args, **kwargs):
149
321
  con = get_server_api_connection()
150
322
  return con.download_file(*args, **kwargs)
@@ -1,15 +1,21 @@
1
+ import re
1
2
  import datetime
2
3
  import uuid
4
+ import string
3
5
  try:
4
6
  from urlparse import urlparse
5
7
  except ImportError:
6
8
  from urllib.parse import urlparse
7
9
 
8
10
  import requests
11
+ import unidecode
12
+
9
13
 
10
14
  from .exceptions import UrlError
11
15
 
12
16
  REMOVED_VALUE = object()
17
+ SLUGIFY_WHITELIST = string.ascii_letters + string.digits
18
+ SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_="
13
19
 
14
20
 
15
21
  def create_entity_id():
@@ -47,6 +53,59 @@ def entity_data_json_default(value):
47
53
  )
48
54
 
49
55
 
56
+ def slugify_string(
57
+ input_string,
58
+ separator="_",
59
+ slug_whitelist=SLUGIFY_WHITELIST,
60
+ split_chars=SLUGIFY_SEP_WHITELIST,
61
+ min_length=1,
62
+ lower=False,
63
+ make_set=False,
64
+ ):
65
+ """Slugify a text string.
66
+
67
+ This function removes transliterates input string to ASCII, removes
68
+ special characters and use join resulting elements using
69
+ specified separator.
70
+
71
+ Args:
72
+ input_string (str): Input string to slugify
73
+ separator (str): A string used to separate returned elements
74
+ (default: "_")
75
+ slug_whitelist (str): Characters allowed in the output
76
+ (default: ascii letters, digits and the separator)
77
+ split_chars (str): Set of characters used for word splitting
78
+ (there is a sane default)
79
+ lower (bool): Convert to lower-case (default: False)
80
+ make_set (bool): Return "set" object instead of string.
81
+ min_length (int): Minimal length of an element (word).
82
+
83
+ Returns:
84
+ Union[str, Set[str]]: Based on 'make_set' value returns slugified
85
+ string.
86
+ """
87
+
88
+ tmp_string = unidecode.unidecode(input_string)
89
+ if lower:
90
+ tmp_string = tmp_string.lower()
91
+
92
+ parts = [
93
+ # Remove all characters that are not in whitelist
94
+ re.sub("[^{}]".format(re.escape(slug_whitelist)), "", part)
95
+ # Split text into part by split characters
96
+ for part in re.split("[{}]".format(re.escape(split_chars)), tmp_string)
97
+ ]
98
+ # Filter text parts by length
99
+ filtered_parts = [
100
+ part
101
+ for part in parts
102
+ if len(part) >= min_length
103
+ ]
104
+ if make_set:
105
+ return set(filtered_parts)
106
+ return separator.join(filtered_parts)
107
+
108
+
50
109
  def failed_json_default(value):
51
110
  return "< Failed value {} > {}".format(type(value), str(value))
52
111
 
@@ -272,8 +331,8 @@ class TransferProgress:
272
331
 
273
332
  def __init__(self):
274
333
  self._started = False
275
- self._done = False
276
- self._downloaded = 0
334
+ self._transfer_done = False
335
+ self._transfered = 0
277
336
  self._content_size = None
278
337
 
279
338
  self._failed = False
@@ -298,15 +357,15 @@ class TransferProgress:
298
357
  raise ValueError("Progress already started")
299
358
  self._started = True
300
359
 
301
- def get_done(self):
302
- return self._done
360
+ def get_transfer_done(self):
361
+ return self._transfer_done
303
362
 
304
- def set_done(self):
305
- if self._done:
363
+ def set_transfer_done(self):
364
+ if self._transfer_done:
306
365
  raise ValueError("Progress was already marked as done")
307
366
  if not self._started:
308
367
  raise ValueError("Progress didn't start yet")
309
- self._done = True
368
+ self._transfer_done = True
310
369
 
311
370
  def get_failed(self):
312
371
  return self._failed
@@ -318,14 +377,14 @@ class TransferProgress:
318
377
  self._fail_reason = reason
319
378
  self._failed = True
320
379
 
321
- def get_downloaded_size(self):
322
- return self._downloaded
380
+ def get_transferred_size(self):
381
+ return self._transfered
323
382
 
324
- def set_downloaded_size(self, downloaded_size):
325
- self._downloaded = downloaded_size
383
+ def set_transferred_size(self, transfered):
384
+ self._transfered = transfered
326
385
 
327
- def add_downloaded_chunk(self, chunk_size):
328
- self._downloaded += chunk_size
386
+ def add_transferred_chunk(self, chunk_size):
387
+ self._transfered += chunk_size
329
388
 
330
389
  def get_source_url(self):
331
390
  return self._source_url
@@ -350,17 +409,17 @@ class TransferProgress:
350
409
  return True
351
410
 
352
411
  @property
353
- def progress(self):
412
+ def transfer_progress(self):
354
413
  if self._content_size is None:
355
414
  return None
356
- return (self._downloaded * 100.0) / float(self._content_size)
415
+ return (self._transfered * 100.0) / float(self._content_size)
357
416
 
358
417
  content_size = property(get_content_size, set_content_size)
359
418
  started = property(get_started)
360
- done = property(get_done)
419
+ transfer_done = property(get_transfer_done)
361
420
  failed = property(get_failed)
362
421
  fail_reason = property(get_fail_reason)
363
422
  source_url = property(get_source_url, set_source_url)
364
423
  destination_url = property(get_destination_url, set_destination_url)
365
424
  content_size = property(get_content_size, set_content_size)
366
- downloaded_size = property(get_downloaded_size, set_downloaded_size)
425
+ transferred_size = property(get_transferred_size, set_transferred_size)
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for Ayon server."""
2
- __version__ = "0.1.7"
2
+ __version__ = "0.1.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -0,0 +1,4 @@
1
+ requests>=2.27.1
2
+ six>=1.15
3
+ Unidecode>=1.3.6
4
+ appdirs<2,>=1
@@ -1,13 +1,14 @@
1
1
  [tool.poetry]
2
2
  name = "ayon_api"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "Python api for AYON server"
5
5
  authors = ["ynput.io <info@ynput.io>"]
6
6
 
7
7
  [tool.poetry.dependencies]
8
8
  requests = "^2.28.1"
9
9
  six = "^1.15"
10
- appdirs = { git = "https://github.com/ActiveState/appdirs.git", branch = "master" }
10
+ Unidecode = "^1.3"
11
+ appdirs = "^1.4"
11
12
 
12
13
  [tool.poetry.dev-dependencies]
13
14
  pytest = "^5.2"
@@ -30,9 +30,8 @@ setup(
30
30
  install_requires=[
31
31
  "requests >= 2.27.1",
32
32
  "six >= 1.15",
33
- ],
34
- dependency_links=[
35
- "appdirs @ git+https://github.com/ActiveState/appdirs.git@master"
33
+ "Unidecode >= 1.3.6",
34
+ "appdirs >=1, <2",
36
35
  ],
37
36
  keywords=["AYON", "ynput", "OpenPype", "vfx"],
38
37
  )
@@ -1 +0,0 @@
1
- appdirs @ git+https://github.com/ActiveState/appdirs.git@master
@@ -1,2 +0,0 @@
1
- requests>=2.27.1
2
- six>=1.15
File without changes