python-alfresco-api 1.1.5__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/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.5.dist-info → python_alfresco_api-1.2.0.dist-info}/METADATA +119 -71
- {python_alfresco_api-1.1.5.dist-info → python_alfresco_api-1.2.0.dist-info}/RECORD +20 -20
- {python_alfresco_api-1.1.5.dist-info → python_alfresco_api-1.2.0.dist-info}/WHEEL +1 -1
- {python_alfresco_api-1.1.5.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}')"
|
|
@@ -85,5 +85,5 @@ class ProbesClient:
|
|
|
85
85
|
|
|
86
86
|
def __repr__(self) -> str:
|
|
87
87
|
"""String representation for debugging."""
|
|
88
|
-
base_url = getattr(self._client_factory, 'base_url', 'unknown')
|
|
88
|
+
base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
|
|
89
89
|
return f"ProbesClient(base_url='{base_url}')"
|
|
@@ -260,5 +260,5 @@ class QueriesClient:
|
|
|
260
260
|
|
|
261
261
|
def __repr__(self) -> str:
|
|
262
262
|
"""String representation for debugging."""
|
|
263
|
-
base_url = getattr(self._client_factory, 'base_url', 'unknown')
|
|
263
|
+
base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
|
|
264
264
|
return f"QueriesClient(base_url='{base_url}')"
|
|
@@ -238,5 +238,5 @@ class RatingsClient:
|
|
|
238
238
|
|
|
239
239
|
def __repr__(self) -> str:
|
|
240
240
|
"""String representation for debugging."""
|
|
241
|
-
base_url = getattr(self._client_factory, 'base_url', 'unknown')
|
|
241
|
+
base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
|
|
242
242
|
return f"RatingsClient(base_url='{base_url}')"
|
|
@@ -153,5 +153,5 @@ class RenditionsClient:
|
|
|
153
153
|
|
|
154
154
|
def __repr__(self) -> str:
|
|
155
155
|
"""String representation for debugging."""
|
|
156
|
-
base_url = getattr(self._client_factory, 'base_url', 'unknown')
|
|
156
|
+
base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
|
|
157
157
|
return f"RenditionsClient(base_url='{base_url}')"
|
|
@@ -31,8 +31,8 @@ class VersionsClient:
|
|
|
31
31
|
return self.parent_client.raw_client
|
|
32
32
|
|
|
33
33
|
def _get_core_client(self):
|
|
34
|
-
"""Get the core client
|
|
35
|
-
return self.
|
|
34
|
+
"""Get the parent core client."""
|
|
35
|
+
return self.parent_client
|
|
36
36
|
|
|
37
37
|
# ==================== SYNC METHODS ====================
|
|
38
38
|
|
|
@@ -110,9 +110,8 @@ class VersionsClient:
|
|
|
110
110
|
CheckoutResponse: Response with checkout details
|
|
111
111
|
"""
|
|
112
112
|
from ....utils import version_utils
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
core_client = self._client_factory.create_core_client()
|
|
113
|
+
|
|
114
|
+
core_client = self._get_core_client()
|
|
116
115
|
|
|
117
116
|
# Use utility function for implementation
|
|
118
117
|
result = version_utils.checkout_document(core_client, node_id)
|
|
@@ -140,9 +139,8 @@ class VersionsClient:
|
|
|
140
139
|
CheckoutResponse: Response with unlock details
|
|
141
140
|
"""
|
|
142
141
|
from ....utils import version_utils
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
core_client = self._client_factory.create_core_client()
|
|
142
|
+
|
|
143
|
+
core_client = self._get_core_client()
|
|
146
144
|
|
|
147
145
|
# Use utility function for implementation
|
|
148
146
|
result = version_utils.cancel_checkout(core_client, node_id)
|
|
@@ -178,9 +176,8 @@ class VersionsClient:
|
|
|
178
176
|
CheckinResponse: Response with new version details
|
|
179
177
|
"""
|
|
180
178
|
from ....utils import version_utils
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
core_client = self._client_factory.create_core_client()
|
|
179
|
+
|
|
180
|
+
core_client = self._get_core_client()
|
|
184
181
|
|
|
185
182
|
# Use utility function for implementation
|
|
186
183
|
result = version_utils.checkin_document(
|
|
@@ -256,46 +253,23 @@ class VersionsClient:
|
|
|
256
253
|
except Exception as e:
|
|
257
254
|
raise ValueError(f"Failed to enable versioning on node {node_id}: {str(e)}")
|
|
258
255
|
|
|
259
|
-
async def checkout_async(self, node_id: str) -> CheckoutResponse:
|
|
256
|
+
async def checkout_async(self, node_id: str, **kwargs) -> CheckoutResponse:
|
|
260
257
|
"""Async version of checkout."""
|
|
261
|
-
|
|
262
|
-
return CheckoutResponse(
|
|
263
|
-
node_id=node_id,
|
|
264
|
-
locked=True,
|
|
265
|
-
working_copy_id=f"{node_id}-working-copy",
|
|
266
|
-
locked_by="current_user",
|
|
267
|
-
locked_at=datetime.now()
|
|
268
|
-
)
|
|
258
|
+
return self.checkout(node_id, **kwargs)
|
|
269
259
|
|
|
270
|
-
async def cancel_checkout_async(self, node_id: str) -> CheckoutResponse:
|
|
260
|
+
async def cancel_checkout_async(self, node_id: str, **kwargs) -> CheckoutResponse:
|
|
271
261
|
"""Async version of cancel_checkout."""
|
|
272
|
-
|
|
273
|
-
return CheckoutResponse(
|
|
274
|
-
node_id=node_id,
|
|
275
|
-
locked=False,
|
|
276
|
-
working_copy_id=None,
|
|
277
|
-
locked_by=None,
|
|
278
|
-
locked_at=None
|
|
279
|
-
)
|
|
262
|
+
return self.cancel_checkout(node_id, **kwargs)
|
|
280
263
|
|
|
281
264
|
async def checkin_async(
|
|
282
265
|
self,
|
|
283
266
|
node_id: str,
|
|
284
267
|
comment: Optional[str] = None,
|
|
285
|
-
major_version: bool = False
|
|
268
|
+
major_version: bool = False,
|
|
269
|
+
**kwargs
|
|
286
270
|
) -> CheckinResponse:
|
|
287
271
|
"""Async version of checkin."""
|
|
288
|
-
|
|
289
|
-
version_number = "2.0" if major_version else "1.1"
|
|
290
|
-
|
|
291
|
-
return CheckinResponse(
|
|
292
|
-
node_id=node_id,
|
|
293
|
-
version_number=version_number,
|
|
294
|
-
comment=comment or "No comment",
|
|
295
|
-
major_version=major_version,
|
|
296
|
-
created_at=datetime.now(),
|
|
297
|
-
created_by="current_user"
|
|
298
|
-
)
|
|
272
|
+
return self.checkin(node_id, comment=comment, major_version=major_version, **kwargs)
|
|
299
273
|
|
|
300
274
|
# ==================== DETAILED METHODS - Moved from AlfrescoCoreClient ====================
|
|
301
275
|
|
|
@@ -399,5 +373,5 @@ class VersionsClient:
|
|
|
399
373
|
|
|
400
374
|
def __repr__(self) -> str:
|
|
401
375
|
"""String representation for debugging."""
|
|
402
|
-
base_url = getattr(self._client_factory, 'base_url', 'unknown')
|
|
376
|
+
base_url = getattr(self.parent_client._client_factory, 'base_url', 'unknown')
|
|
403
377
|
return f"AlfrescoVersionsClient(base_url='{base_url}')"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Alfresco Event System
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Event client for Alfresco's ActiveMQ messaging (repo events on the STOMP topic
|
|
5
|
+
/topic/alfresco.repo.event2). Detects ActiveMQ availability and registers handlers; consumers
|
|
6
|
+
subscribe to the STOMP topic directly (see AlfrescoEventClient for details).
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from .event_client import AlfrescoEventClient
|