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.
- python_alfresco_api/__init__.py +1 -1
- python_alfresco_api/auth_util.py +211 -23
- python_alfresco_api/clients/core/actions/actions_client.py +1 -1
- python_alfresco_api/clients/core/activities/activities_client.py +1 -1
- python_alfresco_api/clients/core/comments/comments_client.py +1 -1
- python_alfresco_api/clients/core/content/content_client.py +1 -1
- python_alfresco_api/clients/core/downloads/downloads_client.py +1 -1
- python_alfresco_api/clients/core/models.py +147 -132
- python_alfresco_api/clients/core/nodes/get_node.py +306 -246
- python_alfresco_api/clients/core/nodes/models.py +264 -264
- python_alfresco_api/clients/core/nodes/nodes_client.py +473 -441
- python_alfresco_api/clients/core/probes/probes_client.py +1 -1
- python_alfresco_api/clients/core/queries/queries_client.py +1 -1
- python_alfresco_api/clients/core/ratings/ratings_client.py +1 -1
- python_alfresco_api/clients/core/renditions/renditions_client.py +1 -1
- python_alfresco_api/clients/core/versions/versions_client.py +16 -42
- python_alfresco_api/events/__init__.py +3 -2
- python_alfresco_api/events/event_client.py +91 -155
- python_alfresco_api/events/models.py +7 -9
- python_alfresco_api/utils/content_utils_highlevel.py +3 -3
- {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/METADATA +119 -71
- {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/RECORD +24 -24
- {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/WHEEL +1 -1
- {python_alfresco_api-1.1.4.dist-info → python_alfresco_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
python_alfresco_api/__init__.py
CHANGED
python_alfresco_api/auth_util.py
CHANGED
|
@@ -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 (
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
]
|