ayon-python-api 1.0.7__tar.gz → 1.0.8__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 (23) hide show
  1. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/PKG-INFO +1 -1
  2. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/__init__.py +10 -0
  3. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/_api.py +23 -0
  4. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/server_api.py +86 -8
  5. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/utils.py +138 -6
  6. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/version.py +1 -1
  7. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_python_api.egg-info/PKG-INFO +1 -1
  8. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/pyproject.toml +2 -2
  9. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/LICENSE +0 -0
  10. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/README.md +0 -0
  11. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/constants.py +0 -0
  12. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/entity_hub.py +0 -0
  13. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/events.py +0 -0
  14. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/exceptions.py +0 -0
  15. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/graphql.py +0 -0
  16. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/graphql_queries.py +0 -0
  17. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_api/operations.py +0 -0
  18. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  19. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  20. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_python_api.egg-info/requires.txt +0 -0
  21. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/ayon_python_api.egg-info/top_level.txt +0 -0
  22. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/setup.cfg +0 -0
  23. {ayon-python-api-1.0.7 → ayon-python-api-1.0.8}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 1.0.7
3
+ Version: 1.0.8
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -3,6 +3,10 @@ from .utils import (
3
3
  TransferProgress,
4
4
  slugify_string,
5
5
  create_dependency_package_basename,
6
+ get_user_by_token,
7
+ is_token_valid,
8
+ validate_url,
9
+ login_to_server,
6
10
  )
7
11
  from .server_api import (
8
12
  RequestTypes,
@@ -67,6 +71,7 @@ from ._api import (
67
71
  download_file,
68
72
  upload_file_from_stream,
69
73
  upload_file,
74
+ upload_reviewable,
70
75
  trigger_server_restart,
71
76
  query_graphql,
72
77
  get_graphql_schema,
@@ -229,6 +234,10 @@ __all__ = (
229
234
  "TransferProgress",
230
235
  "slugify_string",
231
236
  "create_dependency_package_basename",
237
+ "get_user_by_token",
238
+ "is_token_valid",
239
+ "validate_url",
240
+ "login_to_server",
232
241
 
233
242
  "RequestTypes",
234
243
  "ServerAPI",
@@ -290,6 +299,7 @@ __all__ = (
290
299
  "download_file",
291
300
  "upload_file_from_stream",
292
301
  "upload_file",
302
+ "upload_reviewable",
293
303
  "trigger_server_restart",
294
304
  "query_graphql",
295
305
  "get_graphql_schema",
@@ -938,6 +938,29 @@ def upload_file(*args, **kwargs):
938
938
  return con.upload_file(*args, **kwargs)
939
939
 
940
940
 
941
+ def upload_reviewable(*args, **kwargs):
942
+ """Upload reviewable file to server.
943
+
944
+ Args:
945
+ project_name (str): Project name.
946
+ version_id (str): Version id.
947
+ filepath (str): Reviewable file path to upload.
948
+ label (Optional[str]): Reviewable label. Filled automatically
949
+ server side with filename.
950
+ content_type (Optional[str]): MIME type of the file.
951
+ filename (Optional[str]): User as original filename. Filename from
952
+ 'filepath' is used when not filled.
953
+ progress (Optional[TransferProgress]): Progress.
954
+ headers (Optional[Dict[str, Any]]): Headers.
955
+
956
+ Returns:
957
+ RestApiResponse: Server response.
958
+
959
+ """
960
+ con = get_server_api_connection()
961
+ return con.upload_reviewable(*args, **kwargs)
962
+
963
+
941
964
  def trigger_server_restart():
942
965
  """Trigger server restart.
943
966
 
@@ -90,6 +90,7 @@ from .utils import (
90
90
  get_default_settings_variant,
91
91
  get_default_site_id,
92
92
  NOT_SET,
93
+ get_media_mime_type,
93
94
  )
94
95
 
95
96
  PatternType = type(re.compile(""))
@@ -1652,7 +1653,13 @@ class ServerAPI(object):
1652
1653
  response = self.post("enroll", **kwargs)
1653
1654
  if response.status_code == 204:
1654
1655
  return None
1655
- elif response.status_code >= 400:
1656
+
1657
+ if response.status_code == 503:
1658
+ # Server is busy
1659
+ self.log.info("Server is busy. Can't enroll event now.")
1660
+ return None
1661
+
1662
+ if response.status_code >= 400:
1656
1663
  self.log.error(response.text)
1657
1664
  return None
1658
1665
 
@@ -1931,6 +1938,77 @@ class ServerAPI(object):
1931
1938
  endpoint, stream, progress, request_type, **kwargs
1932
1939
  )
1933
1940
 
1941
+ def upload_reviewable(
1942
+ self,
1943
+ project_name,
1944
+ version_id,
1945
+ filepath,
1946
+ label=None,
1947
+ content_type=None,
1948
+ filename=None,
1949
+ progress=None,
1950
+ headers=None,
1951
+ **kwargs
1952
+ ):
1953
+ """Upload reviewable file to server.
1954
+
1955
+ Args:
1956
+ project_name (str): Project name.
1957
+ version_id (str): Version id.
1958
+ filepath (str): Reviewable file path to upload.
1959
+ label (Optional[str]): Reviewable label. Filled automatically
1960
+ server side with filename.
1961
+ content_type (Optional[str]): MIME type of the file.
1962
+ filename (Optional[str]): User as original filename. Filename from
1963
+ 'filepath' is used when not filled.
1964
+ progress (Optional[TransferProgress]): Progress.
1965
+ headers (Optional[Dict[str, Any]]): Headers.
1966
+
1967
+ Returns:
1968
+ RestApiResponse: Server response.
1969
+
1970
+ """
1971
+ if not content_type:
1972
+ content_type = get_media_mime_type(filepath)
1973
+
1974
+ if not content_type:
1975
+ raise ValueError(
1976
+ f"Could not determine MIME type of file '{filepath}'"
1977
+ )
1978
+
1979
+ if headers is None:
1980
+ headers = self.get_headers(content_type)
1981
+ else:
1982
+ # Make sure content-type is filled with file content type
1983
+ content_type_key = next(
1984
+ (
1985
+ key
1986
+ for key in headers
1987
+ if key.lower() == "content-type"
1988
+ ),
1989
+ "Content-Type"
1990
+ )
1991
+ headers[content_type_key] = content_type
1992
+
1993
+ # Fill original filename if not explicitly defined
1994
+ if not filename:
1995
+ filename = os.path.basename(filepath)
1996
+ headers["x-file-name"] = filename
1997
+
1998
+ query = f"?label={label}" if label else ""
1999
+ endpoint = (
2000
+ f"/projects/{project_name}"
2001
+ f"/versions/{version_id}/reviewables{query}"
2002
+ )
2003
+ return self.upload_file(
2004
+ endpoint,
2005
+ filepath,
2006
+ progress=progress,
2007
+ headers=headers,
2008
+ request_type=RequestTypes.post,
2009
+ **kwargs
2010
+ )
2011
+
1934
2012
  def trigger_server_restart(self):
1935
2013
  """Trigger server restart.
1936
2014
 
@@ -8134,11 +8212,11 @@ class ServerAPI(object):
8134
8212
  return op_results
8135
8213
 
8136
8214
  def _convert_entity_data(self, entity):
8137
- if not entity:
8215
+ if not entity or "data" not in entity:
8138
8216
  return
8139
- entity_data = entity.get("data")
8140
- if (
8141
- entity_data is not None
8142
- and isinstance(entity_data, str)
8143
- ):
8144
- entity["data"] = json.loads(entity_data)
8217
+
8218
+ entity_data = entity["data"] or {}
8219
+ if isinstance(entity_data, str):
8220
+ entity_data = json.loads(entity_data)
8221
+
8222
+ entity["data"] = entity_data
@@ -6,6 +6,7 @@ import string
6
6
  import platform
7
7
  import collections
8
8
  from urllib.parse import urlparse, urlencode
9
+ from typing import Optional
9
10
 
10
11
  import requests
11
12
  import unidecode
@@ -348,10 +349,8 @@ def logout_from_server(url, token, timeout=None):
348
349
  )
349
350
 
350
351
 
351
- def is_token_valid(url, token, timeout=None):
352
- """Check if token is valid.
353
-
354
- Token can be a user token or service api key.
352
+ def get_user_by_token(url, token, timeout=None):
353
+ """Get user information by url and token.
355
354
 
356
355
  Args:
357
356
  url (str): Server url.
@@ -360,7 +359,7 @@ def is_token_valid(url, token, timeout=None):
360
359
  'get_default_timeout' is used if not specified.
361
360
 
362
361
  Returns:
363
- bool: True if token is valid.
362
+ Optional[Dict[str, Any]]: User information if url and token are valid.
364
363
 
365
364
  """
366
365
  if timeout is None:
@@ -381,7 +380,27 @@ def is_token_valid(url, token, timeout=None):
381
380
  timeout=timeout,
382
381
  )
383
382
  if response.status_code == 200:
384
- return True
383
+ return response.json()
384
+ return None
385
+
386
+
387
+ def is_token_valid(url, token, timeout=None):
388
+ """Check if token is valid.
389
+
390
+ Token can be a user token or service api key.
391
+
392
+ Args:
393
+ url (str): Server url.
394
+ token (str): User's token.
395
+ timeout (Optional[float]): Timeout for request. Value from
396
+ 'get_default_timeout' is used if not specified.
397
+
398
+ Returns:
399
+ bool: True if token is valid.
400
+
401
+ """
402
+ if get_user_by_token(url, token, timeout=timeout):
403
+ return True
385
404
  return False
386
405
 
387
406
 
@@ -704,3 +723,116 @@ def create_dependency_package_basename(platform_name=None):
704
723
  now_date = datetime.datetime.now()
705
724
  time_stamp = now_date.strftime("%y%m%d%H%M")
706
725
  return "ayon_{}_{}".format(time_stamp, platform_name)
726
+
727
+
728
+
729
+ def _get_media_mime_type_from_ftyp(content):
730
+ if content[8:10] == b"qt" or content[8:12] == b"MSNV":
731
+ return "video/quicktime"
732
+
733
+ if content[8:12] in (b"3g2a", b"3g2b", b"3g2c", b"KDDI"):
734
+ return "video/3gpp2"
735
+
736
+ if content[8:12] in (
737
+ b"isom", b"iso2", b"avc1", b"F4V", b"F4P", b"F4A", b"F4B", b"mmp4",
738
+ # These might be "video/mp4v"
739
+ b"mp41", b"mp42",
740
+ # Nero
741
+ b"NDSC", b"NDSH", b"NDSM", b"NDSP", b"NDSS", b"NDXC", b"NDXH",
742
+ b"NDXM", b"NDXP", b"NDXS",
743
+ ):
744
+ return "video/mp4"
745
+
746
+ if content[8:12] in (
747
+ b"3ge6", b"3ge7", b"3gg6",
748
+ b"3gp1", b"3gp2", b"3gp3", b"3gp4", b"3gp5", b"3gp6", b"3gs7",
749
+ ):
750
+ return "video/3gpp"
751
+
752
+ if content[8:11] == b"JP2":
753
+ return "image/jp2"
754
+
755
+ if content[8:11] == b"jpm":
756
+ return "image/jpm"
757
+
758
+ if content[8:11] == b"jpx":
759
+ return "image/jpx"
760
+
761
+ if content[8:12] in (b"M4V\x20", b"M4VH", b"M4VP"):
762
+ return "video/x-m4v"
763
+
764
+ if content[8:12] in (b"mj2s", b"mjp2"):
765
+ return "video/mj2"
766
+ return None
767
+
768
+
769
+ def get_media_mime_type_for_content(content: bytes) -> Optional[str]:
770
+ content_len = len(content)
771
+ # Pre-validation (largest definition check)
772
+ # - hopefully there cannot be media defined in less than 12 bytes
773
+ if content_len < 12:
774
+ return None
775
+
776
+ # FTYP
777
+ if content[4:8] == b"ftyp":
778
+ return _get_media_mime_type_from_ftyp(content)
779
+
780
+ # BMP
781
+ if content[0:2] == b"BM":
782
+ return "image/bmp"
783
+
784
+ # Tiff
785
+ if content[0:2] in (b"MM", b"II"):
786
+ return "tiff"
787
+
788
+ # PNG
789
+ if content[0:4] == b"\211PNG":
790
+ return "image/png"
791
+
792
+ # SVG
793
+ if b'xmlns="http://www.w3.org/2000/svg"' in content:
794
+ return "image/svg+xml"
795
+
796
+ # JPEG, JFIF or Exif
797
+ if (
798
+ content[0:4] == b"\xff\xd8\xff\xdb"
799
+ or content[6:10] in (b"JFIF", b"Exif")
800
+ ):
801
+ return "image/jpeg"
802
+
803
+ # Webp
804
+ if content[0:4] == b"RIFF" and content[8:12] == b"WEBP":
805
+ return "image/webp"
806
+
807
+ # Gif
808
+ if content[0:6] in (b"GIF87a", b"GIF89a"):
809
+ return "gif"
810
+
811
+ # Adobe PhotoShop file (8B > Adobe, PS > PhotoShop)
812
+ if content[0:4] == b"8BPS":
813
+ return "image/vnd.adobe.photoshop"
814
+
815
+ # Windows ICO > this might be wild guess as multiple files can start
816
+ # with this header
817
+ if content[0:4] == b"\x00\x00\x01\x00":
818
+ return "image/x-icon"
819
+ return None
820
+
821
+
822
+ def get_media_mime_type(filepath: str) -> Optional[str]:
823
+ """Determine Mime-Type of a file.
824
+
825
+ Args:
826
+ filepath (str): Path to file.
827
+
828
+ Returns:
829
+ Optional[str]: Mime type or None if is unknown mime type.
830
+
831
+ """
832
+ if not filepath or not os.path.exists(filepath):
833
+ return None
834
+
835
+ with open(filepath, "rb") as stream:
836
+ content = stream.read()
837
+
838
+ return get_media_mime_type_for_content(content)
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for AYON server."""
2
- __version__ = "1.0.7"
2
+ __version__ = "1.0.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 1.0.7
3
+ Version: 1.0.8
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ayon-python-api"
3
- version = "1.0.7"
3
+ version = "1.0.8"
4
4
  description = "AYON Python API"
5
5
  license = {file = "LICENSE"}
6
6
  readme = {file = "README.md", content-type = "text/markdown"}
@@ -29,7 +29,7 @@ build-backend = "poetry.core.masonry.api"
29
29
 
30
30
  [tool.poetry]
31
31
  name = "ayon-python-api"
32
- version = "1.0.7"
32
+ version = "1.0.8"
33
33
  description = "AYON Python API"
34
34
  authors = [
35
35
  "ynput.io <info@ynput.io>"
File without changes