Ayon-Python-Api 0.3.3__tar.gz → 0.3.4__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 (24) hide show
  1. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/PKG-INFO +2 -3
  2. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/README.md +1 -2
  3. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/entity_hub.py +744 -4
  4. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/server_api.py +33 -13
  5. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/version.py +1 -1
  6. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_python_api.egg-info/PKG-INFO +2 -3
  7. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/pyproject.toml +1 -1
  8. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/LICENSE +0 -0
  9. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/__init__.py +0 -0
  10. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/_api.py +0 -0
  11. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/constants.py +0 -0
  12. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/events.py +0 -0
  13. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/exceptions.py +0 -0
  14. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/graphql.py +0 -0
  15. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/graphql_queries.py +0 -0
  16. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/operations.py +0 -0
  17. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/thumbnails.py +0 -0
  18. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_api/utils.py +0 -0
  19. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_python_api.egg-info/SOURCES.txt +0 -0
  20. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_python_api.egg-info/dependency_links.txt +0 -0
  21. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_python_api.egg-info/requires.txt +0 -0
  22. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/ayon_python_api.egg-info/top_level.txt +0 -0
  23. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/setup.cfg +0 -0
  24. {ayon-python-api-0.3.3 → ayon-python-api-0.3.4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -273,8 +273,7 @@ If pip complain that `ayon-python-api` is already installed just uninstall exist
273
273
  - Move entity hub to first place
274
274
  - Skip those which are invalid for the entity and fake it for base or remove it from base
275
275
  - Entity hub should use operations session to do changes
276
- - Entity hub could also handle 'subset', 'version' and 'representation' entities
277
- - Missing 'statuses' on project
276
+ - Entity hub could also handle 'product', 'version' and 'representation' entities
278
277
  - Missing 'status' on folders
279
278
  - Missing assignees on tasks
280
279
  - Pass docstrings and arguments definitions from `ServerAPI` methods to global functions
@@ -54,8 +54,7 @@ If pip complain that `ayon-python-api` is already installed just uninstall exist
54
54
  - Move entity hub to first place
55
55
  - Skip those which are invalid for the entity and fake it for base or remove it from base
56
56
  - Entity hub should use operations session to do changes
57
- - Entity hub could also handle 'subset', 'version' and 'representation' entities
58
- - Missing 'statuses' on project
57
+ - Entity hub could also handle 'product', 'version' and 'representation' entities
59
58
  - Missing 'status' on folders
60
59
  - Missing assignees on tasks
61
60
  - Pass docstrings and arguments definitions from `ServerAPI` methods to global functions
@@ -1,10 +1,11 @@
1
+ import re
1
2
  import copy
2
3
  import collections
3
4
  from abc import ABCMeta, abstractmethod
4
5
 
5
6
  import six
6
7
  from ._api import get_server_api_connection
7
- from .utils import create_entity_id, convert_entity_id
8
+ from .utils import create_entity_id, convert_entity_id, slugify_string
8
9
 
9
10
  UNKNOWN_VALUE = object()
10
11
  PROJECT_PARENT_ID = object()
@@ -545,6 +546,7 @@ class EntityHub(object):
545
546
  library=project["library"],
546
547
  folder_types=project["folderTypes"],
547
548
  task_types=project["taskTypes"],
549
+ statuses=project["statuses"],
548
550
  name=project["name"],
549
551
  attribs=project["ownAttrib"],
550
552
  data=project["data"],
@@ -775,8 +777,7 @@ class EntityHub(object):
775
777
  "projects/{}".format(self.project_name),
776
778
  **project_changes
777
779
  )
778
- if response.status_code != 204:
779
- raise ValueError("Failed to update project")
780
+ response.raise_for_status()
780
781
 
781
782
  self.project_entity.lock()
782
783
 
@@ -1485,6 +1486,722 @@ class BaseEntity(object):
1485
1486
  self._children_ids = set(children_ids)
1486
1487
 
1487
1488
 
1489
+ class ProjectStatus:
1490
+ """Project status class.
1491
+
1492
+ Args:
1493
+ name (str): Name of the status. e.g. 'In progress'
1494
+ short_name (Optional[str]): Short name of the status. e.g. 'IP'
1495
+ state (Optional[Literal[not_started, in_progress, done, blocked]]): A
1496
+ state of the status.
1497
+ icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
1498
+ color (Optional[str]): Color of the status. e.g. '#eeeeee'.
1499
+ index (Optional[int]): Index of the status.
1500
+ project_statuses (Optional[_ProjectStatuses]): Project statuses
1501
+ wrapper.
1502
+ """
1503
+
1504
+ valid_states = ("not_started", "in_progress", "done", "blocked")
1505
+ color_regex = re.compile(r"#([a-f0-9]{6})$")
1506
+ default_state = "in_progress"
1507
+ default_color = "#eeeeee"
1508
+
1509
+ def __init__(
1510
+ self,
1511
+ name,
1512
+ short_name=None,
1513
+ state=None,
1514
+ icon=None,
1515
+ color=None,
1516
+ index=None,
1517
+ project_statuses=None,
1518
+ is_new=None,
1519
+ ):
1520
+ short_name = short_name or ""
1521
+ icon = icon or ""
1522
+ state = state or self.default_state
1523
+ color = color or self.default_color
1524
+ self._name = name
1525
+ self._short_name = short_name
1526
+ self._icon = icon
1527
+ self._slugified_name = None
1528
+ self._state = None
1529
+ self._color = None
1530
+ self.set_state(state)
1531
+ self.set_color(color)
1532
+
1533
+ self._original_name = name
1534
+ self._original_short_name = short_name
1535
+ self._original_icon = icon
1536
+ self._original_state = state
1537
+ self._original_color = color
1538
+ self._original_index = index
1539
+
1540
+ self._index = index
1541
+ self._project_statuses = project_statuses
1542
+ if is_new is None:
1543
+ is_new = index is None or project_statuses is None
1544
+ self._is_new = is_new
1545
+
1546
+ def __str__(self):
1547
+ short_name = ""
1548
+ if self.short_name:
1549
+ short_name = "({})".format(self.short_name)
1550
+ return "<{} {}{}>".format(
1551
+ self.__class__.__name__, self.name, short_name
1552
+ )
1553
+
1554
+ def __repr__(self):
1555
+ return str(self)
1556
+
1557
+ def __getitem__(self, key):
1558
+ if key in {
1559
+ "name", "short_name", "icon", "state", "color", "slugified_name"
1560
+ }:
1561
+ return getattr(self, key)
1562
+ raise KeyError(key)
1563
+
1564
+ def __setitem__(self, key, value):
1565
+ if key in {"name", "short_name", "icon", "state", "color"}:
1566
+ return setattr(self, key, value)
1567
+ raise KeyError(key)
1568
+
1569
+ def lock(self):
1570
+ """Lock status.
1571
+
1572
+ Changes were commited and current values are now the original values.
1573
+ """
1574
+
1575
+ self._is_new = False
1576
+ self._original_name = self.name
1577
+ self._original_short_name = self.short_name
1578
+ self._original_icon = self.icon
1579
+ self._original_state = self.state
1580
+ self._original_color = self.color
1581
+ self._original_index = self.index
1582
+
1583
+ @staticmethod
1584
+ def slugify_name(name):
1585
+ """Slugify status name for name comparison.
1586
+
1587
+ Args:
1588
+ name (str): Name of the status.
1589
+
1590
+ Returns:
1591
+ str: Slugified name.
1592
+ """
1593
+
1594
+ return slugify_string(name.lower())
1595
+
1596
+ def get_project_statuses(self):
1597
+ """Internal logic method.
1598
+
1599
+ Returns:
1600
+ _ProjectStatuses: Project statuses object.
1601
+ """
1602
+
1603
+ return self._project_statuses
1604
+
1605
+ def set_project_statuses(self, project_statuses):
1606
+ """Internal logic method to change parent object.
1607
+
1608
+ Args:
1609
+ project_statuses (_ProjectStatuses): Project statuses object.
1610
+ """
1611
+
1612
+ self._project_statuses = project_statuses
1613
+
1614
+ def unset_project_statuses(self, project_statuses):
1615
+ """Internal logic method to unset parent object.
1616
+
1617
+ Args:
1618
+ project_statuses (_ProjectStatuses): Project statuses object.
1619
+ """
1620
+
1621
+ if self._project_statuses is project_statuses:
1622
+ self._project_statuses = None
1623
+ self._index = None
1624
+
1625
+ @property
1626
+ def changed(self):
1627
+ """Status has changed.
1628
+
1629
+ Returns:
1630
+ bool: Status has changed.
1631
+ """
1632
+
1633
+ return (
1634
+ self._is_new
1635
+ or self._original_name != self._name
1636
+ or self._original_short_name != self._short_name
1637
+ or self._original_index != self._index
1638
+ or self._original_state != self._state
1639
+ or self._original_icon != self._icon
1640
+ or self._original_color != self._color
1641
+ )
1642
+
1643
+ def delete(self):
1644
+ """Remove status from project statuses object."""
1645
+
1646
+ if self._project_statuses is not None:
1647
+ self._project_statuses.remove(self)
1648
+
1649
+ def get_index(self):
1650
+ """Get index of status.
1651
+
1652
+ Returns:
1653
+ Union[int, None]: Index of status or None if status is not under
1654
+ project.
1655
+ """
1656
+
1657
+ return self._index
1658
+
1659
+ def set_index(self, index, **kwargs):
1660
+ """Change status index.
1661
+
1662
+ Returns:
1663
+ Union[int, None]: Index of status or None if status is not under
1664
+ project.
1665
+ """
1666
+
1667
+ if kwargs.get("from_parent"):
1668
+ self._index = index
1669
+ else:
1670
+ self._project_statuses.set_status_index(self, index)
1671
+
1672
+ def get_name(self):
1673
+ """Status name.
1674
+
1675
+ Returns:
1676
+ str: Status name.
1677
+ """
1678
+
1679
+ return self._name
1680
+
1681
+ def set_name(self, name):
1682
+ """Change status name.
1683
+
1684
+ Args:
1685
+ name (str): New status name.
1686
+ """
1687
+
1688
+ if not isinstance(name, six.string_types):
1689
+ raise TypeError("Name must be a string.")
1690
+ if name == self._name:
1691
+ return
1692
+ self._name = name
1693
+ self._slugified_name = None
1694
+
1695
+ def get_short_name(self):
1696
+ """Status short name 3 letters tops.
1697
+
1698
+ Returns:
1699
+ str: Status short name.
1700
+ """
1701
+
1702
+ return self._short_name
1703
+
1704
+ def set_short_name(self, short_name):
1705
+ """Change status short name.
1706
+
1707
+ Args:
1708
+ short_name (str): New status short name. 3 letters tops.
1709
+ """
1710
+
1711
+ if not isinstance(short_name, six.string_types):
1712
+ raise TypeError("Short name must be a string.")
1713
+ self._short_name = short_name
1714
+
1715
+ def get_icon(self):
1716
+ """Name of icon to use for status.
1717
+
1718
+ Returns:
1719
+ str: Name of the icon.
1720
+ """
1721
+
1722
+ return self._icon
1723
+
1724
+ def set_icon(self, icon):
1725
+ """Change status icon name.
1726
+
1727
+ Args:
1728
+ icon (str): Name of the icon.
1729
+ """
1730
+
1731
+ if icon is None:
1732
+ icon = ""
1733
+ if not isinstance(icon, six.string_types):
1734
+ raise TypeError("Icon name must be a string.")
1735
+ self._icon = icon
1736
+
1737
+ @property
1738
+ def slugified_name(self):
1739
+ """Slugified and lowere status name.
1740
+
1741
+ Can be used for comparison of existing statuses. e.g. 'In Progress'
1742
+ vs. 'in-progress'.
1743
+
1744
+ Returns:
1745
+ str: Slugified and lower status name.
1746
+ """
1747
+
1748
+ if self._slugified_name is None:
1749
+ self._slugified_name = self.slugify_name(self.name)
1750
+ return self._slugified_name
1751
+
1752
+ def get_state(self):
1753
+ """Get state of project status.
1754
+
1755
+ Return:
1756
+ Literal[not_started, in_progress, done, blocked]: General
1757
+ state of status.
1758
+ """
1759
+
1760
+ return self._state
1761
+
1762
+ def set_state(self, state):
1763
+ """Set color of project status.
1764
+
1765
+ Args:
1766
+ state (Literal[not_started, in_progress, done, blocked]): General
1767
+ state of status.
1768
+ """
1769
+
1770
+ if state not in self.valid_states:
1771
+ raise ValueError("Invalid state '{}'".format(str(state)))
1772
+ self._state = state
1773
+
1774
+ def get_color(self):
1775
+ """Get color of project status.
1776
+
1777
+ Returns:
1778
+ str: Status color.
1779
+ """
1780
+
1781
+ return self._color
1782
+
1783
+ def set_color(self, color):
1784
+ """Set color of project status.
1785
+
1786
+ Args:
1787
+ color (str): Color in hex format. Example: '#ff0000'.
1788
+ """
1789
+
1790
+ if not isinstance(color, six.string_types):
1791
+ raise TypeError(
1792
+ "Color must be string got '{}'".format(type(color)))
1793
+ color = color.lower()
1794
+ if self.color_regex.fullmatch(color) is None:
1795
+ raise ValueError("Invalid color value '{}'".format(color))
1796
+ self._color = color
1797
+
1798
+ name = property(get_name, set_name)
1799
+ short_name = property(get_short_name, set_short_name)
1800
+ project_statuses = property(get_project_statuses, set_project_statuses)
1801
+ index = property(get_index, set_index)
1802
+ state = property(get_state, set_state)
1803
+ color = property(get_color, set_color)
1804
+ icon = property(get_icon, set_icon)
1805
+
1806
+ def _validate_other_p_statuses(self, other):
1807
+ """Validate if other status can be used for move.
1808
+
1809
+ To be able to work with other status, and position them in relation,
1810
+ they must belong to same existing object of '_ProjectStatuses'.
1811
+
1812
+ Args:
1813
+ other (ProjectStatus): Other status to validate.
1814
+ """
1815
+
1816
+ o_project_statuses = other.project_statuses
1817
+ m_project_statuses = self.project_statuses
1818
+ if o_project_statuses is None and m_project_statuses is None:
1819
+ raise ValueError("Both statuses are not assigned to a project.")
1820
+
1821
+ missing_status = None
1822
+ if o_project_statuses is None:
1823
+ missing_status = other
1824
+ elif m_project_statuses is None:
1825
+ missing_status = self
1826
+ if missing_status is not None:
1827
+ raise ValueError(
1828
+ "Status '{}' is not assigned to a project.".format(
1829
+ missing_status.name))
1830
+ if m_project_statuses is not o_project_statuses:
1831
+ raise ValueError(
1832
+ "Statuse are assigned to different projects."
1833
+ " Cannot execute move."
1834
+ )
1835
+
1836
+ def move_before(self, other):
1837
+ """Move status before other status.
1838
+
1839
+ Args:
1840
+ other (ProjectStatus): Status to move before.
1841
+ """
1842
+
1843
+ self._validate_other_p_statuses(other)
1844
+ self._project_statuses.set_status_index(self, other.index)
1845
+
1846
+ def move_after(self, other):
1847
+ """Move status after other status.
1848
+
1849
+ Args:
1850
+ other (ProjectStatus): Status to move after.
1851
+ """
1852
+
1853
+ self._validate_other_p_statuses(other)
1854
+ self._project_statuses.set_status_index(self, other.index + 1)
1855
+
1856
+ def to_data(self):
1857
+ """Convert status to data.
1858
+
1859
+ Returns:
1860
+ dict[str, str]: Status data.
1861
+ """
1862
+
1863
+ output = {
1864
+ "name": self.name,
1865
+ "shortName": self.short_name,
1866
+ "state": self.state,
1867
+ "icon": self.icon,
1868
+ "color": self.color,
1869
+ }
1870
+ if (
1871
+ not self._is_new
1872
+ and self._original_name
1873
+ and self.name != self._original_name
1874
+ ):
1875
+ output["original_name"] = self._original_name
1876
+ return output
1877
+
1878
+ @classmethod
1879
+ def from_data(cls, data, index=None, project_statuses=None):
1880
+ """Create project status from data.
1881
+
1882
+ Args:
1883
+ data (dict[str, str]): Status data.
1884
+ index (Optional[int]): Status index.
1885
+ project_statuses (Optional[ProjectStatuses]): Project statuses
1886
+ object which wraps the status for a project.
1887
+ """
1888
+
1889
+ return cls(
1890
+ data["name"],
1891
+ data.get("shortName", data.get("short_name")),
1892
+ data.get("state"),
1893
+ data.get("icon"),
1894
+ data.get("color"),
1895
+ index=index,
1896
+ project_statuses=project_statuses
1897
+ )
1898
+
1899
+
1900
+ class _ProjectStatuses:
1901
+ """Wrapper for project statuses.
1902
+
1903
+ Supports basic methods to add, change or remove statuses from a project.
1904
+
1905
+ To add new statuses use 'create' or 'add_status' methods. To change
1906
+ statuses receive them by one of the getter methods and change their
1907
+ values.
1908
+
1909
+ Todos:
1910
+ Validate if statuses are duplicated.
1911
+ """
1912
+
1913
+ def __init__(self, statuses):
1914
+ self._statuses = [
1915
+ ProjectStatus.from_data(status, idx, self)
1916
+ for idx, status in enumerate(statuses)
1917
+ ]
1918
+ self._orig_status_length = len(self._statuses)
1919
+ self._set_called = False
1920
+
1921
+ def __len__(self):
1922
+ return len(self._statuses)
1923
+
1924
+ def __iter__(self):
1925
+ """Iterate over statuses.
1926
+
1927
+ Yields:
1928
+ ProjectStatus: Project status.
1929
+ """
1930
+
1931
+ for status in self._statuses:
1932
+ yield status
1933
+
1934
+ def create(
1935
+ self,
1936
+ name,
1937
+ short_name=None,
1938
+ state=None,
1939
+ icon=None,
1940
+ color=None,
1941
+ ):
1942
+ """Create project status.
1943
+
1944
+ Args:
1945
+ name (str): Name of the status. e.g. 'In progress'
1946
+ short_name (Optional[str]): Short name of the status. e.g. 'IP'
1947
+ state (Optional[Literal[not_started, in_progress, done, blocked]]): A
1948
+ state of the status.
1949
+ icon (Optional[str]): Icon of the status. e.g. 'play_arrow'.
1950
+ color (Optional[str]): Color of the status. e.g. '#eeeeee'.
1951
+
1952
+ Returns:
1953
+ ProjectStatus: Created project status.
1954
+ """
1955
+
1956
+ status = ProjectStatus(
1957
+ name, short_name, state, icon, color, is_new=True
1958
+ )
1959
+ self.append(status)
1960
+ return status
1961
+
1962
+ def lock(self):
1963
+ """Lock statuses.
1964
+
1965
+ Changes were commited and current values are now the original values.
1966
+ """
1967
+
1968
+ self._orig_status_length = len(self._statuses)
1969
+ self._set_called = False
1970
+ for status in self._statuses:
1971
+ status.lock()
1972
+
1973
+ def to_data(self):
1974
+ """Convert to project statuses data."""
1975
+
1976
+ return [
1977
+ status.to_data()
1978
+ for status in self._statuses
1979
+ ]
1980
+
1981
+ def set(self, statuses):
1982
+ """Explicitly override statuses.
1983
+
1984
+ This method does not handle if statuses changed or not.
1985
+
1986
+ Args:
1987
+ statuses (list[dict[str, str]]): List of statuses data.
1988
+ """
1989
+
1990
+ self._set_called = True
1991
+ self._statuses = [
1992
+ ProjectStatus.from_data(status, idx, self)
1993
+ for idx, status in enumerate(statuses)
1994
+ ]
1995
+
1996
+ @property
1997
+ def changed(self):
1998
+ """Statuses have changed.
1999
+
2000
+ Returns:
2001
+ bool: True if statuses changed, False otherwise.
2002
+ """
2003
+
2004
+ if self._set_called:
2005
+ return True
2006
+
2007
+ # Check if status length changed
2008
+ # - when all statuses are removed it is a changed
2009
+ if self._orig_status_length != len(self._statuses):
2010
+ return True
2011
+ # Go through all statuses and check if any of them changed
2012
+ for status in self._statuses:
2013
+ if status.changed:
2014
+ return True
2015
+ return False
2016
+
2017
+ def get(self, name, default=None):
2018
+ """Get status by name.
2019
+
2020
+ Args:
2021
+ name (str): Status name.
2022
+ default (Any): Default value of status is not found.
2023
+
2024
+ Returns:
2025
+ Union[ProjectStatus, Any]: Status or default value.
2026
+ """
2027
+
2028
+ return next(
2029
+ (
2030
+ status
2031
+ for status in self._statuses
2032
+ if status.name == name
2033
+ ),
2034
+ default
2035
+ )
2036
+
2037
+ get_status_by_name = get
2038
+
2039
+ def index(self, status, **kwargs):
2040
+ """Get status index.
2041
+
2042
+ Args:
2043
+ status (ProjectStatus): Status to get index of.
2044
+ default (Optional[Any]): Default value if status is not found.
2045
+
2046
+ Returns:
2047
+ Union[int, Any]: Status index.
2048
+
2049
+ Raises:
2050
+ ValueError: If status is not found and default value is not
2051
+ defined.
2052
+ """
2053
+
2054
+ output = next(
2055
+ (
2056
+ idx
2057
+ for idx, st in enumerate(self._statuses)
2058
+ if st is status
2059
+ ),
2060
+ None
2061
+ )
2062
+ if output is not None:
2063
+ return output
2064
+
2065
+ if "default" in kwargs:
2066
+ return kwargs["default"]
2067
+ raise ValueError("Status '{}' not found".format(status.name))
2068
+
2069
+ def get_status_by_slugified_name(self, name):
2070
+ """Get status by slugified name.
2071
+
2072
+ Args:
2073
+ name (str): Status name. Is slugified before search.
2074
+
2075
+ Returns:
2076
+ Union[ProjectStatus, None]: Status or None if not found.
2077
+ """
2078
+
2079
+ slugified_name = ProjectStatus.slugify_name(name)
2080
+ return next(
2081
+ (
2082
+ status
2083
+ for status in self._statuses
2084
+ if status.slugified_name == slugified_name
2085
+ ),
2086
+ None
2087
+ )
2088
+
2089
+ def remove_by_name(self, name, ignore_missing=False):
2090
+ """Remove status by name.
2091
+
2092
+ Args:
2093
+ name (str): Status name.
2094
+ ignore_missing (Optional[bool]): If True, no error is raised if
2095
+ status is not found.
2096
+
2097
+ Returns:
2098
+ ProjectStatus: Removed status.
2099
+ """
2100
+
2101
+ matching_status = self.get(name)
2102
+ if matching_status is None:
2103
+ if ignore_missing:
2104
+ return
2105
+ raise ValueError(
2106
+ "Status '{}' not found in project".format(name))
2107
+ return self.remove(matching_status)
2108
+
2109
+ def remove(self, status, ignore_missing=False):
2110
+ """Remove status.
2111
+
2112
+ Args:
2113
+ status (ProjectStatus): Status to remove.
2114
+ ignore_missing (Optional[bool]): If True, no error is raised if
2115
+ status is not found.
2116
+
2117
+ Returns:
2118
+ Union[ProjectStatus, None]: Removed status.
2119
+ """
2120
+
2121
+ index = self.index(status, default=None)
2122
+ if index is None:
2123
+ if ignore_missing:
2124
+ return None
2125
+ raise ValueError("Status '{}' not in project".format(status))
2126
+
2127
+ return self.pop(index)
2128
+
2129
+ def pop(self, index):
2130
+ """Remove status by index.
2131
+
2132
+ Args:
2133
+ index (int): Status index.
2134
+
2135
+ Returns:
2136
+ ProjectStatus: Removed status.
2137
+ """
2138
+
2139
+ status = self._statuses.pop(index)
2140
+ status.unset_project_statuses(self)
2141
+ for st in self._statuses[index:]:
2142
+ st.set_index(st.index - 1, from_parent=True)
2143
+ return status
2144
+
2145
+ def insert(self, index, status):
2146
+ """Insert status at index.
2147
+
2148
+ Args:
2149
+ index (int): Status index.
2150
+ status (Union[ProjectStatus, dict[str, str]]): Status to insert.
2151
+ Can be either status object or status data.
2152
+
2153
+ Returns:
2154
+ ProjectStatus: Inserted status.
2155
+ """
2156
+
2157
+ if not isinstance(status, ProjectStatus):
2158
+ status = ProjectStatus.from_data(status)
2159
+
2160
+ start_index = index
2161
+ end_index = len(self._statuses) + 1
2162
+ matching_index = self.index(status, default=None)
2163
+ if matching_index is not None:
2164
+ if matching_index == index:
2165
+ status.set_index(index, from_parent=True)
2166
+ return
2167
+
2168
+ self._statuses.pop(matching_index)
2169
+ if matching_index < index:
2170
+ start_index = matching_index
2171
+ end_index = index + 1
2172
+ else:
2173
+ end_index -= 1
2174
+
2175
+ status.set_project_statuses(self)
2176
+ self._statuses.insert(index, status)
2177
+ for idx, st in enumerate(self._statuses[start_index:end_index]):
2178
+ st.set_index(start_index + idx, from_parent=True)
2179
+ return status
2180
+
2181
+ def append(self, status):
2182
+ """Add new status to the end of the list.
2183
+
2184
+ Args:
2185
+ status (Union[ProjectStatus, dict[str, str]]): Status to insert.
2186
+ Can be either status object or status data.
2187
+
2188
+ Returns:
2189
+ ProjectStatus: Inserted status.
2190
+ """
2191
+
2192
+ return self.insert(len(self._statuses), status)
2193
+
2194
+ def set_status_index(self, status, index):
2195
+ """Set status index.
2196
+
2197
+ Args:
2198
+ status (ProjectStatus): Status to set index.
2199
+ index (int): New status index.
2200
+ """
2201
+
2202
+ return self.insert(index, status)
2203
+
2204
+
1488
2205
  class ProjectEntity(BaseEntity):
1489
2206
  """Entity representing project on AYON server.
1490
2207
 
@@ -1514,7 +2231,14 @@ class ProjectEntity(BaseEntity):
1514
2231
  default_task_type_icon = "task_alt"
1515
2232
 
1516
2233
  def __init__(
1517
- self, project_code, library, folder_types, task_types, *args, **kwargs
2234
+ self,
2235
+ project_code,
2236
+ library,
2237
+ folder_types,
2238
+ task_types,
2239
+ statuses,
2240
+ *args,
2241
+ **kwargs
1518
2242
  ):
1519
2243
  super(ProjectEntity, self).__init__(*args, **kwargs)
1520
2244
 
@@ -1522,11 +2246,13 @@ class ProjectEntity(BaseEntity):
1522
2246
  self._library_project = library
1523
2247
  self._folder_types = folder_types
1524
2248
  self._task_types = task_types
2249
+ self._statuses_obj = _ProjectStatuses(statuses)
1525
2250
 
1526
2251
  self._orig_project_code = project_code
1527
2252
  self._orig_library_project = library
1528
2253
  self._orig_folder_types = copy.deepcopy(folder_types)
1529
2254
  self._orig_task_types = copy.deepcopy(task_types)
2255
+ self._orig_statuses = copy.deepcopy(statuses)
1530
2256
 
1531
2257
  def _prepare_entity_id(self, entity_id):
1532
2258
  if entity_id != self.project_name:
@@ -1573,13 +2299,24 @@ class ProjectEntity(BaseEntity):
1573
2299
  new_task_types.append(task_type)
1574
2300
  self._task_types = new_task_types
1575
2301
 
2302
+ def get_orig_statuses(self):
2303
+ return copy.deepcopy(self._orig_statuses)
2304
+
2305
+ def get_statuses(self):
2306
+ return self._statuses_obj
2307
+
2308
+ def set_statuses(self, statuses):
2309
+ self._statuses_obj.set(statuses)
2310
+
1576
2311
  folder_types = property(get_folder_types, set_folder_types)
1577
2312
  task_types = property(get_task_types, set_task_types)
2313
+ statuses = property(get_statuses, set_statuses)
1578
2314
 
1579
2315
  def lock(self):
1580
2316
  super(ProjectEntity, self).lock()
1581
2317
  self._orig_folder_types = copy.deepcopy(self._folder_types)
1582
2318
  self._orig_task_types = copy.deepcopy(self._task_types)
2319
+ self._statuses_obj.lock()
1583
2320
 
1584
2321
  @property
1585
2322
  def changes(self):
@@ -1590,6 +2327,9 @@ class ProjectEntity(BaseEntity):
1590
2327
  if self._orig_task_types != self._task_types:
1591
2328
  changes["taskTypes"] = self.get_task_types()
1592
2329
 
2330
+ if self._statuses_obj.changed:
2331
+ changes["statuses"] = self._statuses_obj.to_data()
2332
+
1593
2333
  return changes
1594
2334
 
1595
2335
  @classmethod
@@ -14,7 +14,16 @@ except ImportError:
14
14
  HTTPStatus = None
15
15
 
16
16
  import requests
17
- from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
17
+ try:
18
+ # This should be used if 'requests' have it available
19
+ from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError
20
+ except ImportError:
21
+ # Older versions of 'requests' don't have custom exception for json
22
+ # decode error
23
+ try:
24
+ from simplejson import JSONDecodeError as RequestsJSONDecodeError
25
+ except ImportError:
26
+ from json import JSONDecodeError as RequestsJSONDecodeError
18
27
 
19
28
  from .constants import (
20
29
  DEFAULT_PRODUCT_TYPE_FIELDS,
@@ -900,18 +909,24 @@ class ServerAPI(object):
900
909
 
901
910
  self.validate_server_availability()
902
911
 
903
- response = self.post(
904
- "auth/login",
905
- name=username,
906
- password=password
907
- )
908
- if response.status_code != 200:
909
- _detail = response.data.get("detail")
910
- details = ""
911
- if _detail:
912
- details = " {}".format(_detail)
912
+ self._token_validation_started = True
913
+
914
+ try:
915
+ response = self.post(
916
+ "auth/login",
917
+ name=username,
918
+ password=password
919
+ )
920
+ if response.status_code != 200:
921
+ _detail = response.data.get("detail")
922
+ details = ""
923
+ if _detail:
924
+ details = " {}".format(_detail)
925
+
926
+ raise AuthenticationError("Login failed {}".format(details))
913
927
 
914
- raise AuthenticationError("Login failed {}".format(details))
928
+ finally:
929
+ self._token_validation_started = False
915
930
 
916
931
  self._access_token = response["token"]
917
932
 
@@ -1228,7 +1243,8 @@ class ServerAPI(object):
1228
1243
  target_topic,
1229
1244
  sender,
1230
1245
  description=None,
1231
- sequential=None
1246
+ sequential=None,
1247
+ events_filter=None,
1232
1248
  ):
1233
1249
  """Enroll job based on events.
1234
1250
 
@@ -1270,6 +1286,8 @@ class ServerAPI(object):
1270
1286
  in target event.
1271
1287
  sequential (Optional[bool]): The source topic must be processed
1272
1288
  in sequence.
1289
+ events_filter (Optional[ayon_server.sqlfilter.Filter]): A dict-like
1290
+ with conditions to filter the source event.
1273
1291
 
1274
1292
  Returns:
1275
1293
  Union[None, dict[str, Any]]: None if there is no event matching
@@ -1285,6 +1303,8 @@ class ServerAPI(object):
1285
1303
  kwargs["sequential"] = sequential
1286
1304
  if description is not None:
1287
1305
  kwargs["description"] = description
1306
+ if events_filter is not None:
1307
+ kwargs["filter"] = events_filter
1288
1308
  response = self.post("enroll", **kwargs)
1289
1309
  if response.status_code == 204:
1290
1310
  return None
@@ -1,2 +1,2 @@
1
1
  """Package declaring Python API for Ayon server."""
2
- __version__ = "0.3.3"
2
+ __version__ = "0.3.4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ayon-python-api
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: AYON Python API
5
5
  Home-page: https://github.com/ynput/ayon-python-api
6
6
  Author: ynput.io
@@ -273,8 +273,7 @@ If pip complain that `ayon-python-api` is already installed just uninstall exist
273
273
  - Move entity hub to first place
274
274
  - Skip those which are invalid for the entity and fake it for base or remove it from base
275
275
  - Entity hub should use operations session to do changes
276
- - Entity hub could also handle 'subset', 'version' and 'representation' entities
277
- - Missing 'statuses' on project
276
+ - Entity hub could also handle 'product', 'version' and 'representation' entities
278
277
  - Missing 'status' on folders
279
278
  - Missing assignees on tasks
280
279
  - Pass docstrings and arguments definitions from `ServerAPI` methods to global functions
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ayon-python-api"
3
- version = "0.3.3"
3
+ version = "0.3.4"
4
4
  description = "AYON Python API"
5
5
  license = {file = "LICENSE"}
6
6
  readme = {file = "README.md", content-type = "text/markdown"}
File without changes