python-alfresco-api 1.1.4__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. python_alfresco_api/__init__.py +1 -1
  2. python_alfresco_api/auth_util.py +211 -23
  3. python_alfresco_api/clients/core/actions/actions_client.py +1 -1
  4. python_alfresco_api/clients/core/activities/activities_client.py +1 -1
  5. python_alfresco_api/clients/core/comments/comments_client.py +1 -1
  6. python_alfresco_api/clients/core/content/content_client.py +1 -1
  7. python_alfresco_api/clients/core/downloads/downloads_client.py +1 -1
  8. python_alfresco_api/clients/core/models.py +147 -132
  9. python_alfresco_api/clients/core/nodes/get_node.py +306 -246
  10. python_alfresco_api/clients/core/nodes/models.py +264 -264
  11. python_alfresco_api/clients/core/nodes/nodes_client.py +473 -441
  12. python_alfresco_api/clients/core/probes/probes_client.py +1 -1
  13. python_alfresco_api/clients/core/queries/queries_client.py +1 -1
  14. python_alfresco_api/clients/core/ratings/ratings_client.py +1 -1
  15. python_alfresco_api/clients/core/renditions/renditions_client.py +1 -1
  16. python_alfresco_api/clients/core/versions/versions_client.py +16 -42
  17. python_alfresco_api/events/__init__.py +3 -2
  18. python_alfresco_api/events/event_client.py +91 -155
  19. python_alfresco_api/events/models.py +7 -9
  20. python_alfresco_api/utils/content_utils_highlevel.py +3 -3
  21. {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/METADATA +119 -71
  22. {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/RECORD +24 -24
  23. {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/WHEEL +1 -1
  24. {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -36,7 +36,7 @@ from .clients.conversion_utils import (
36
36
  # Import raw_clients to ensure they get packaged
37
37
  from . import raw_clients
38
38
 
39
- __version__ = "1.1.1"
39
+ __version__ = "1.2.0"
40
40
  __all__ = [
41
41
  # Factory & utilities
42
42
  "ClientFactory",
@@ -7,6 +7,7 @@ Handles ticket-based authentication with automatic renewal.
7
7
 
8
8
  import asyncio
9
9
  import base64
10
+ import json
10
11
  import os
11
12
  from typing import Optional, Dict, Any, Union
12
13
  from datetime import datetime, timedelta
@@ -276,31 +277,77 @@ class TicketAuthUtil:
276
277
  Handles both Basic auth and ticket-based authentication.
277
278
  """
278
279
 
279
- def __init__(self, username: str, password: str, base_url: str = ""):
280
+ def __init__(self, username: str, password: str, base_url: str = "",
281
+ verify_ssl: Union[bool, str] = True, timeout: Optional[int] = None):
280
282
  """
281
283
  Initialize ticket auth utility.
282
-
284
+
283
285
  Args:
284
286
  username: Alfresco username
285
- password: Alfresco password
286
- base_url: Base URL for Alfresco (optional, for future ticket API calls)
287
+ password: Alfresco password
288
+ base_url: Base URL for Alfresco (required to self-fetch a ticket)
289
+ verify_ssl: SSL verification - True, False, or path to certificate bundle
290
+ timeout: Request timeout in seconds (None = use system defaults)
287
291
  """
288
292
  self.username = username
289
293
  self.password = password
290
294
  self.base_url = base_url.rstrip('/')
295
+ self.verify_ssl = verify_ssl
296
+ self.timeout = timeout
291
297
  self.ticket = None
298
+ self.ticket_expires = None
292
299
  self._authenticated = False
293
-
300
+
301
+ def authenticate_sync(self) -> bool:
302
+ """Synchronously fetch an Alfresco login ticket from username/password."""
303
+ if not self.base_url:
304
+ return False
305
+ try:
306
+ auth_url = f"{self.base_url}/alfresco/api/-default-/public/authentication/versions/1/tickets"
307
+ client_kwargs = {"verify": self.verify_ssl}
308
+ if self.timeout is not None:
309
+ client_kwargs["timeout"] = self.timeout
310
+ with httpx.Client(**client_kwargs) as client:
311
+ response = client.post(auth_url, json={"userId": self.username, "password": self.password})
312
+ if response.status_code == 201:
313
+ self.ticket = response.json()["entry"]["id"]
314
+ self.ticket_expires = datetime.now() + timedelta(hours=1)
315
+ self._authenticated = True
316
+ return True
317
+ print(f"Alfresco ticket authentication failed: {response.status_code} {response.text}")
318
+ except Exception as e:
319
+ print(f"Alfresco ticket authentication failed: {e}")
320
+ self._authenticated = False
321
+ return False
322
+
323
+ def ensure_authenticated_sync(self) -> bool:
324
+ """Ensure a valid ticket, fetching one if needed."""
325
+ if self.is_authenticated():
326
+ return True
327
+ return self.authenticate_sync()
328
+
294
329
  def get_basic_auth_header(self):
295
330
  """Get basic auth header for initial authentication."""
296
331
  auth_string = f"{self.username}:{self.password}"
297
332
  auth_b64 = base64.b64encode(auth_string.encode()).decode()
298
333
  return f'Basic {auth_b64}'
299
-
334
+
300
335
  def get_auth_token(self):
301
- """Get just the base64 auth token (without 'Basic ' prefix) for AuthenticatedClient."""
302
- auth_string = f"{self.username}:{self.password}"
303
- return base64.b64encode(auth_string.encode()).decode()
336
+ """Get the base64 token (no 'Basic ' prefix) for AuthenticatedClient.
337
+
338
+ Alfresco accepts a login ticket as `Authorization: Basic base64(<ticket>)`. We lazily
339
+ fetch a ticket (when base_url is set) and return base64(ticket); if that fails we fall
340
+ back to base64(username:password) so the client still authenticates.
341
+ """
342
+ if not self.is_authenticated() and self.base_url:
343
+ try:
344
+ self.ensure_authenticated_sync()
345
+ except Exception as e:
346
+ print(f"Alfresco ticket acquisition failed: {e}")
347
+ if self.is_authenticated() and self.ticket:
348
+ return base64.b64encode(self.ticket.encode()).decode()
349
+ # Fallback: basic credentials
350
+ return base64.b64encode(f"{self.username}:{self.password}".encode()).decode()
304
351
 
305
352
  def get_ticket_header(self):
306
353
  """Get ticket auth header if we have a ticket."""
@@ -332,8 +379,13 @@ class TicketAuthUtil:
332
379
  self._authenticated = True
333
380
 
334
381
  def is_authenticated(self):
335
- """Check if we have a valid ticket."""
336
- return self._authenticated and self.ticket is not None
382
+ """Check if we have a valid (unexpired) ticket."""
383
+ if not self._authenticated or not self.ticket:
384
+ return False
385
+ if self.ticket_expires and datetime.now() >= self.ticket_expires:
386
+ self._authenticated = False
387
+ return False
388
+ return True
337
389
 
338
390
  class OAuth2AuthUtil:
339
391
  """
@@ -416,7 +468,18 @@ class OAuth2AuthUtil:
416
468
 
417
469
  # For environment variable loading
418
470
  self._load_oauth_env_config(load_env, env_file)
419
-
471
+
472
+ # If an access token was supplied (constructor or env), mark it usable so the
473
+ # synchronous client-build path can use it immediately. Previously a provided token
474
+ # was ignored because _authenticated stayed False.
475
+ if self.access_token:
476
+ self._authenticated = True
477
+ # Derive expiry from the token's own JWT `exp` claim when no expires_in was given,
478
+ # so a *provided* (pasted) token that has since expired is detected as stale and
479
+ # refreshed via refresh_token — instead of being used blindly and 401ing.
480
+ if self.token_expires is None:
481
+ self.token_expires = self._expiry_from_jwt(self.access_token)
482
+
420
483
  def _load_oauth_env_config(self, load_env: bool, env_file: Optional[str]):
421
484
  """Load OAuth2-specific configuration from environment."""
422
485
  if not load_env:
@@ -569,26 +632,143 @@ class OAuth2AuthUtil:
569
632
 
570
633
  self._authenticated = True
571
634
  return True
572
-
635
+
573
636
  return False
574
-
637
+
638
+ @staticmethod
639
+ def _expiry_from_jwt(token: str) -> Optional[datetime]:
640
+ """Best-effort read of a JWT access token's `exp` claim as a local naive datetime.
641
+
642
+ Lets a provided access token (which carries no expires_in) still be expiry-checked so
643
+ it can be refreshed when stale. Returns None for opaque (non-JWT) tokens or on any error.
644
+ """
645
+ try:
646
+ parts = token.split(".")
647
+ if len(parts) != 3:
648
+ return None
649
+ payload_b64 = parts[1]
650
+ payload_b64 += "=" * (-len(payload_b64) % 4) # restore base64 padding
651
+ payload = json.loads(base64.urlsafe_b64decode(payload_b64))
652
+ exp = payload.get("exp")
653
+ return datetime.fromtimestamp(exp) if exp else None
654
+ except Exception:
655
+ return None
656
+
657
+ def _apply_token_response(self, token_data: Dict[str, Any]) -> None:
658
+ """Apply an OAuth2 token-endpoint response to this util's token state."""
659
+ self.access_token = token_data.get("access_token")
660
+ expires_in = token_data.get("expires_in")
661
+ if expires_in:
662
+ self.token_expires = datetime.now() + timedelta(seconds=expires_in)
663
+ elif self.access_token:
664
+ self.token_expires = self._expiry_from_jwt(self.access_token)
665
+ if "refresh_token" in token_data:
666
+ self.refresh_token = token_data["refresh_token"]
667
+ self._authenticated = bool(self.access_token)
668
+
669
+ # --- Synchronous token acquisition ---------------------------------------
670
+ # The sub-clients call get_auth_token() synchronously at client-build time, so the async
671
+ # authenticate()/ensure_authenticated() above never fire on their own. These sync variants
672
+ # let get_auth_token() lazily acquire/refresh a token so OAuth2 works end-to-end without the
673
+ # caller having to pre-await authenticate().
674
+
675
+ def _client_credentials_flow_sync(self) -> bool:
676
+ """Synchronous OAuth2 client-credentials flow."""
677
+ if not self.client_id or not self.client_secret or not self.token_endpoint:
678
+ raise ValueError(
679
+ "Client credentials flow requires client_id, client_secret, and token_endpoint"
680
+ )
681
+ data = {
682
+ "grant_type": "client_credentials",
683
+ "client_id": self.client_id,
684
+ "client_secret": self.client_secret,
685
+ }
686
+ if self.scope:
687
+ data["scope"] = self.scope
688
+ client_kwargs = {"verify": self.verify_ssl}
689
+ if self.timeout is not None:
690
+ client_kwargs["timeout"] = self.timeout
691
+ with httpx.Client(**client_kwargs) as client:
692
+ response = client.post(
693
+ self.token_endpoint, data=data,
694
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
695
+ )
696
+ if response.status_code == 200:
697
+ self._apply_token_response(response.json())
698
+ return True
699
+ return False
700
+
701
+ def _refresh_token_flow_sync(self) -> bool:
702
+ """Synchronous refresh-token flow."""
703
+ if not self.refresh_token or not self.token_endpoint:
704
+ return False
705
+ data = {
706
+ "grant_type": "refresh_token",
707
+ "refresh_token": self.refresh_token,
708
+ "client_id": self.client_id,
709
+ }
710
+ if self.client_secret:
711
+ data["client_secret"] = self.client_secret
712
+ client_kwargs = {"verify": self.verify_ssl}
713
+ if self.timeout is not None:
714
+ client_kwargs["timeout"] = self.timeout
715
+ with httpx.Client(**client_kwargs) as client:
716
+ response = client.post(
717
+ self.token_endpoint, data=data,
718
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
719
+ )
720
+ if response.status_code == 200:
721
+ self._apply_token_response(response.json())
722
+ return True
723
+ return False
724
+
725
+ def authenticate_sync(self) -> bool:
726
+ """Synchronous authenticate() — dispatches by grant type."""
727
+ try:
728
+ if self.grant_type == "client_credentials":
729
+ return self._client_credentials_flow_sync()
730
+ elif self.grant_type == "refresh_token" and self.refresh_token:
731
+ return self._refresh_token_flow_sync()
732
+ elif self.grant_type == "authorization_code":
733
+ # Requires prior user interaction; usable only if an access_token was supplied.
734
+ return self.is_authenticated()
735
+ else:
736
+ raise ValueError(f"Unsupported grant type: {self.grant_type}")
737
+ except Exception as e:
738
+ print(f"OAuth2 authentication failed: {e}")
739
+ self._authenticated = False
740
+ return False
741
+
742
+ def ensure_authenticated_sync(self) -> bool:
743
+ """Synchronous ensure_authenticated() — refresh if possible, else authenticate."""
744
+ if self.is_authenticated():
745
+ return True
746
+ if self.refresh_token:
747
+ if self._refresh_token_flow_sync():
748
+ return True
749
+ return self.authenticate_sync()
750
+
575
751
  def is_authenticated(self) -> bool:
576
752
  """Check if currently authenticated with valid access token."""
577
753
  if not self._authenticated or not self.access_token:
578
754
  return False
579
755
 
580
- # Check token expiration
581
- if self.token_expires and datetime.now() >= self.token_expires:
756
+ # Check token expiration (30s skew so we refresh just before it actually expires)
757
+ if self.token_expires and datetime.now() >= (self.token_expires - timedelta(seconds=30)):
582
758
  self._authenticated = False
583
759
  return False
584
-
760
+
585
761
  return True
586
762
 
587
763
  def get_auth_headers(self) -> Dict[str, str]:
588
- """Get authentication headers for API requests."""
764
+ """Get authentication headers for API requests (lazily acquiring a token if needed)."""
765
+ if not self.is_authenticated():
766
+ try:
767
+ self.ensure_authenticated_sync()
768
+ except Exception as e:
769
+ print(f"OAuth2 token acquisition failed: {e}")
589
770
  if not self.is_authenticated():
590
771
  return {}
591
-
592
772
  return {
593
773
  "Authorization": f"Bearer {self.access_token}"
594
774
  }
@@ -600,10 +780,18 @@ class OAuth2AuthUtil:
600
780
  return ""
601
781
 
602
782
  def get_auth_token(self):
603
- """Get just the access token (without 'Bearer ' prefix) for AuthenticatedClient."""
604
- if self.is_authenticated():
605
- return self.access_token
606
- return ""
783
+ """Get the access token (no 'Bearer ' prefix) for AuthenticatedClient.
784
+
785
+ Lazily acquires/refreshes the token synchronously when needed, so the sub-clients'
786
+ synchronous build path produces a real Bearer header. Previously this returned "" unless
787
+ the caller had manually pre-awaited authenticate(), which nothing in the build path did.
788
+ """
789
+ if not self.is_authenticated():
790
+ try:
791
+ self.ensure_authenticated_sync()
792
+ except Exception as e:
793
+ print(f"OAuth2 token acquisition failed: {e}")
794
+ return self.access_token if self.is_authenticated() else ""
607
795
 
608
796
  def get_auth_prefix(self):
609
797
  """Get authentication prefix for headers. OAuth2AuthUtil uses Bearer tokens."""
@@ -303,5 +303,5 @@ class ActionsClient:
303
303
 
304
304
  def __repr__(self) -> str:
305
305
  """String representation for debugging."""
306
- base_url = getattr(self._client_factory, 'base_url', 'unknown')
306
+ base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
307
307
  return f"AlfrescoActionsClient(base_url='{base_url}')"
@@ -124,5 +124,5 @@ class ActivitiesClient:
124
124
 
125
125
  def __repr__(self) -> str:
126
126
  """String representation for debugging."""
127
- base_url = getattr(self._client_factory, 'base_url', 'unknown')
127
+ base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
128
128
  return f"AlfrescoActivitiesClient(base_url='{base_url}')"
@@ -303,5 +303,5 @@ class CommentsClient:
303
303
 
304
304
  def __repr__(self) -> str:
305
305
  """String representation for debugging."""
306
- base_url = getattr(self._client_factory, 'base_url', 'unknown')
306
+ base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
307
307
  return f"AlfrescoCommentsClient(base_url='{base_url}')"
@@ -458,5 +458,5 @@ class ContentClient:
458
458
 
459
459
  def __repr__(self) -> str:
460
460
  """String representation for debugging."""
461
- base_url = getattr(self._client_factory, 'base_url', 'unknown')
461
+ base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
462
462
  return f"AlfrescoContentClient(base_url='{base_url}')"
@@ -173,5 +173,5 @@ class DownloadsClient:
173
173
 
174
174
  def __repr__(self) -> str:
175
175
  """String representation for debugging."""
176
- base_url = getattr(self._client_factory, 'base_url', 'unknown')
176
+ base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
177
177
  return f"DownloadsClient(base_url='{base_url}')"
@@ -1,133 +1,148 @@
1
- """
2
- Level 2: Core API Models - Shared within Core API
3
-
4
- This module contains models and structures that are specific to the
5
- Alfresco Core API but shared across multiple Core operations.
6
-
7
- Three-Tier Architecture:
8
- - Level 1: Global models shared across ALL APIs
9
- - Level 2 (This file): Core API models shared within Core API
10
- - Level 3: Operation-specific models for specific operations
11
-
12
- Benefits:
13
- - Perfect locality (Core-specific models in Core namespace)
14
- - Clean imports (from .models import CoreResponse)
15
- - Logical organization (API-level grouping)
16
- - Maintainability (focused scope)
17
- """
18
-
19
- from typing import Optional, List, Dict, Any, Union
20
- from datetime import datetime
21
- from pydantic import BaseModel, Field, ConfigDict
22
- from enum import Enum
23
-
24
- # Import global models
25
- from ..models import BaseEntry, PagingInfo, ContentInfo, UserInfo
26
-
27
-
28
- class CoreResponse(BaseModel):
29
- """Base response wrapper for Core API operations."""
30
- model_config = ConfigDict(extra='forbid')
31
-
32
- entry: Optional[BaseEntry] = Field(None, description="Single entry response")
33
-
34
-
35
- class CoreEntryList(BaseModel):
36
- """List response wrapper for Core API operations."""
37
- model_config = ConfigDict(extra='forbid')
38
-
39
- entries: List[BaseEntry] = Field(default_factory=list, description="List of entries")
40
- pagination: Optional[PagingInfo] = Field(None, description="Pagination information")
41
-
42
-
43
- class Permission(str, Enum):
44
- """Alfresco permission levels for Core API."""
45
- READ = "Read"
46
- WRITE = "Write"
47
- DELETE = "Delete"
48
- CHANGE_PERMISSIONS = "ChangePermissions"
49
- FULL_CONTROL = "FullControl"
50
- COORDINATOR = "SiteCoordinator"
51
- COLLABORATOR = "SiteCollaborator"
52
- CONTRIBUTOR = "SiteContributor"
53
- CONSUMER = "SiteConsumer"
54
-
55
-
56
- class NodeType(str, Enum):
57
- """Alfresco content model node types."""
58
- CONTENT = "cm:content"
59
- FOLDER = "cm:folder"
60
- PERSON = "cm:person"
61
- GROUP = "cm:authorityContainer"
62
- SITE = "st:site"
63
-
64
-
65
- class VersionType(str, Enum):
66
- """Version types for Core API operations."""
67
- MAJOR = "MAJOR"
68
- MINOR = "MINOR"
69
-
70
-
71
- class SortOrder(str, Enum):
72
- """Sort order options."""
73
- ASC = "ASC"
74
- DESC = "DESC"
75
-
76
-
77
- class IncludeOption(str, Enum):
78
- """Include options for Core API requests."""
79
- PROPERTIES = "properties"
80
- PERMISSIONS = "permissions"
81
- PATH = "path"
82
- CHILDREN = "children"
83
- ASSOCIATION = "association"
84
- ASPECTS = "aspects"
85
- IS_LINK = "isLink"
86
- IS_LOCKED = "isLocked"
87
- ALLOW_ABLE_OPERATIONS = "allowableOperations"
88
-
89
-
90
- class CorePermissionInfo(BaseModel):
91
- """Permission information for Core API objects."""
92
- model_config = ConfigDict(extra='forbid')
93
-
94
- authority_id: str = Field(..., description="Authority (user/group) identifier")
95
- authority_type: str = Field(..., description="Type of authority (USER, GROUP)")
96
- permission: Permission = Field(..., description="Permission level")
97
- access_status: str = Field(..., description="Access status (ALLOWED, DENIED)")
98
-
99
-
100
- class CoreAspectInfo(BaseModel):
101
- """Aspect information for Core API objects."""
102
- model_config = ConfigDict(extra='forbid')
103
-
104
- aspect_name: str = Field(..., description="Aspect name (e.g., cm:titled)")
105
- title: Optional[str] = Field(None, description="Aspect title")
106
- description: Optional[str] = Field(None, description="Aspect description")
107
-
108
-
109
- class CorePathElement(BaseModel):
110
- """Path element for Core API path structures."""
111
- model_config = ConfigDict(extra='allow') # Allow extra fields from Alfresco
112
-
113
- id: str = Field(..., description="Node ID")
114
- name: str = Field(..., description="Node name")
115
- node_type: Optional[str] = Field(None, alias="nodeType", description="Node type")
116
- aspect_names: Optional[List[str]] = Field(None, alias="aspectNames", description="Aspect names")
117
-
118
-
119
- class CorePathInfo(BaseModel):
120
- """Path information for Core API objects."""
121
- model_config = ConfigDict(extra='allow') # Allow extra fields from Alfresco
122
-
123
- elements: List[CorePathElement] = Field(default_factory=list, description="Path elements")
124
- name: Optional[str] = Field(None, description="Display name")
125
- is_complete: Optional[bool] = Field(None, alias="isComplete", description="Whether path is complete")
126
-
127
-
128
- # Export all models
129
- __all__ = [
130
- 'CoreResponse', 'CoreEntryList', 'Permission', 'NodeType', 'VersionType',
131
- 'SortOrder', 'IncludeOption', 'CorePermissionInfo', 'CoreAspectInfo',
132
- 'CorePathElement', 'CorePathInfo'
1
+ """
2
+ Level 2: Core API Models - Shared within Core API
3
+
4
+ This module contains models and structures that are specific to the
5
+ Alfresco Core API but shared across multiple Core operations.
6
+
7
+ Three-Tier Architecture:
8
+ - Level 1: Global models shared across ALL APIs
9
+ - Level 2 (This file): Core API models shared within Core API
10
+ - Level 3: Operation-specific models for specific operations
11
+
12
+ Benefits:
13
+ - Perfect locality (Core-specific models in Core namespace)
14
+ - Clean imports (from .models import CoreResponse)
15
+ - Logical organization (API-level grouping)
16
+ - Maintainability (focused scope)
17
+ """
18
+
19
+ from typing import Optional, List, Dict, Any, Union
20
+ from datetime import datetime
21
+ from pydantic import BaseModel, Field, ConfigDict
22
+ from enum import Enum
23
+
24
+ # Import global models
25
+ from ..models import BaseEntry, PagingInfo, ContentInfo, UserInfo
26
+
27
+
28
+ class CoreResponse(BaseModel):
29
+ """Base response wrapper for Core API operations."""
30
+ model_config = ConfigDict(extra='forbid')
31
+
32
+ entry: Optional[BaseEntry] = Field(None, description="Single entry response")
33
+
34
+
35
+ class CoreEntryList(BaseModel):
36
+ """List response wrapper for Core API operations."""
37
+ model_config = ConfigDict(extra='forbid')
38
+
39
+ entries: List[BaseEntry] = Field(default_factory=list, description="List of entries")
40
+ pagination: Optional[PagingInfo] = Field(None, description="Pagination information")
41
+
42
+
43
+ class Permission(str, Enum):
44
+ """Alfresco permission levels for Core API."""
45
+ READ = "Read"
46
+ WRITE = "Write"
47
+ DELETE = "Delete"
48
+ CHANGE_PERMISSIONS = "ChangePermissions"
49
+ FULL_CONTROL = "FullControl"
50
+ COORDINATOR = "SiteCoordinator"
51
+ COLLABORATOR = "SiteCollaborator"
52
+ CONTRIBUTOR = "SiteContributor"
53
+ CONSUMER = "SiteConsumer"
54
+
55
+
56
+ class NodeType:
57
+ """
58
+ Alfresco content model node types.
59
+
60
+ This class provides constants for common node types while allowing
61
+ custom types (e.g., 'my:customDocument', 'acme:invoice').
62
+
63
+ Use as a namespace for common types:
64
+ NodeType.CONTENT # "cm:content"
65
+ NodeType.FOLDER # "cm:folder"
66
+
67
+ Or use any custom string directly:
68
+ node_type = "my:customDocument" # Works with custom content models
69
+ """
70
+ CONTENT = "cm:content"
71
+ FOLDER = "cm:folder"
72
+ PERSON = "cm:person"
73
+ GROUP = "cm:authorityContainer"
74
+ SITE = "st:site"
75
+
76
+ # Common custom types (for reference)
77
+ # Users can use any string value for their custom types
78
+
79
+
80
+ class VersionType(str, Enum):
81
+ """Version types for Core API operations."""
82
+ MAJOR = "MAJOR"
83
+ MINOR = "MINOR"
84
+
85
+
86
+ class SortOrder(str, Enum):
87
+ """Sort order options."""
88
+ ASC = "ASC"
89
+ DESC = "DESC"
90
+
91
+
92
+ class IncludeOption(str, Enum):
93
+ """Include options for Core API requests."""
94
+ PROPERTIES = "properties"
95
+ PERMISSIONS = "permissions"
96
+ PATH = "path"
97
+ CHILDREN = "children"
98
+ ASSOCIATION = "association"
99
+ ASPECTS = "aspects"
100
+ IS_LINK = "isLink"
101
+ IS_LOCKED = "isLocked"
102
+ ALLOW_ABLE_OPERATIONS = "allowableOperations"
103
+
104
+
105
+ class CorePermissionInfo(BaseModel):
106
+ """Permission information for Core API objects."""
107
+ model_config = ConfigDict(extra='forbid')
108
+
109
+ authority_id: str = Field(..., description="Authority (user/group) identifier")
110
+ authority_type: str = Field(..., description="Type of authority (USER, GROUP)")
111
+ permission: Permission = Field(..., description="Permission level")
112
+ access_status: str = Field(..., description="Access status (ALLOWED, DENIED)")
113
+
114
+
115
+ class CoreAspectInfo(BaseModel):
116
+ """Aspect information for Core API objects."""
117
+ model_config = ConfigDict(extra='forbid')
118
+
119
+ aspect_name: str = Field(..., description="Aspect name (e.g., cm:titled)")
120
+ title: Optional[str] = Field(None, description="Aspect title")
121
+ description: Optional[str] = Field(None, description="Aspect description")
122
+
123
+
124
+ class CorePathElement(BaseModel):
125
+ """Path element for Core API path structures."""
126
+ model_config = ConfigDict(extra='allow') # Allow extra fields from Alfresco
127
+
128
+ id: str = Field(..., description="Node ID")
129
+ name: str = Field(..., description="Node name")
130
+ node_type: Optional[str] = Field(None, alias="nodeType", description="Node type")
131
+ aspect_names: Optional[List[str]] = Field(None, alias="aspectNames", description="Aspect names")
132
+
133
+
134
+ class CorePathInfo(BaseModel):
135
+ """Path information for Core API objects."""
136
+ model_config = ConfigDict(extra='allow') # Allow extra fields from Alfresco
137
+
138
+ elements: List[CorePathElement] = Field(default_factory=list, description="Path elements")
139
+ name: Optional[str] = Field(None, description="Display name")
140
+ is_complete: Optional[bool] = Field(None, alias="isComplete", description="Whether path is complete")
141
+
142
+
143
+ # Export all models
144
+ __all__ = [
145
+ 'CoreResponse', 'CoreEntryList', 'Permission', 'NodeType', 'VersionType',
146
+ 'SortOrder', 'IncludeOption', 'CorePermissionInfo', 'CoreAspectInfo',
147
+ 'CorePathElement', 'CorePathInfo'
133
148
  ]