71
71
default_config_path = "~/.artifactory_python.cfg"
72
72
global_config = None
73
73
74
+ # Pathlib.Path changed significantly in 3.12, so we will not need several
75
+ # parts of the code once python3.11 is no longer supported. This constant helps
76
+ # identifying those.
77
+ _IS_PYTHON_3_12_OR_NEWER = sys .version_info >= (3 , 12 )
78
+
74
79
75
80
def read_config (config_path = default_config_path ):
76
81
"""
@@ -422,7 +427,7 @@ def quote_url(url):
422
427
return quoted_url
423
428
424
429
425
- class _ArtifactoryFlavour (pathlib ._Flavour ):
430
+ class _ArtifactoryFlavour (object if _IS_PYTHON_3_12_OR_NEWER else pathlib ._Flavour ):
426
431
"""
427
432
Implements Artifactory-specific pure path manipulations.
428
433
I.e. what is 'drive', 'root' and 'path' and how to split full path into
@@ -432,7 +437,7 @@ class _ArtifactoryFlavour(pathlib._Flavour):
432
437
drive: in context of artifactory, it's the base URI like
433
438
http://mysite/artifactory
434
439
435
- root: repository, e.g. 'libs-snapshot-local' or 'ext-release-local'
440
+ root: like in unix, / when absolute, empty when relative
436
441
437
442
path: relative artifact path within the repository
438
443
"""
@@ -458,13 +463,6 @@ def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2):
458
463
drv , root , parts , drv2 , root2 , parts2
459
464
)
460
465
461
- if not root2 and len (parts2 ) > 1 :
462
- root2 = self .sep + parts2 .pop (1 ) + self .sep
463
-
464
- # quick hack for https://github.com/devopshq/artifactory/issues/29
465
- # drive or repository must start with / , if not - add it
466
- if not drv2 .endswith ("/" ) and not root2 .startswith ("/" ):
467
- drv2 = drv2 + self .sep
468
466
return drv2 , root2 , parts2
469
467
470
468
def splitroot (self , part , sep = sep ):
@@ -501,7 +499,7 @@ def splitroot(self, part, sep=sep):
501
499
502
500
if url .path is None or url .path == sep :
503
501
if url .scheme :
504
- return part .rstrip (sep ), "" , ""
502
+ return part .rstrip (sep ), "/ " , ""
505
503
return "" , "" , part
506
504
elif url .path .lstrip ("/" ).startswith ("artifactory" ):
507
505
mark = sep + "artifactory" + sep
@@ -510,8 +508,8 @@ def splitroot(self, part, sep=sep):
510
508
path = self ._get_path (part )
511
509
drv = part .rpartition (path )[0 ]
512
510
path_parts = path .strip (sep ).split (sep )
513
- root = sep + path_parts [ 0 ] + sep
514
- rest = sep .join (path_parts [1 :])
511
+ root = sep
512
+ rest = sep .join (path_parts [0 :])
515
513
return drv , root , rest
516
514
517
515
if len (parts ) >= 2 :
@@ -524,14 +522,14 @@ def splitroot(self, part, sep=sep):
524
522
rest = part
525
523
526
524
if not rest :
527
- return drv , "" , ""
525
+ return drv , "/ " , ""
528
526
529
527
if rest == sep :
530
- return drv , "" , ""
528
+ return drv , "/ " , ""
531
529
532
530
if rest .startswith (sep ):
533
- root , _ , part = rest [ 1 :]. partition ( sep )
534
- root = sep + root + sep
531
+ root = sep
532
+ part = rest . lstrip ( "/" )
535
533
536
534
return drv , root , part
537
535
@@ -587,6 +585,29 @@ def make_uri(self, path):
587
585
"""
588
586
return path
589
587
588
+ def normcase (self , path ):
589
+ return path
590
+
591
+ def splitdrive (self , path ):
592
+ drv , root , part = self .splitroot (path )
593
+ return (drv + root , self .sep .join (part ))
594
+
595
+ # This function is consumed by PurePath._load_parts() after python 3.12
596
+ def join (self , path , * paths ):
597
+ drv , root , part = self .splitroot (path )
598
+
599
+ for next_path in paths :
600
+ drv2 , root2 , part2 = self .splitroot (next_path )
601
+ if drv2 != "" :
602
+ drv , root , part = drv2 , root2 , part2
603
+ continue
604
+ if root2 != "" :
605
+ root , part = root2 , part2
606
+ continue
607
+ part = part + self .sep + part2
608
+
609
+ return drv + root + part
610
+
590
611
591
612
class _ArtifactorySaaSFlavour (_ArtifactoryFlavour ):
592
613
def _get_base_url (self , url ):
@@ -877,7 +898,11 @@ def get_stat_json(self, pathobj, key=None):
877
898
)
878
899
code = response .status_code
879
900
text = response .text
880
- if code == 404 and ("Unable to find item" in text or "Not Found" in text or "File not found" in text ):
901
+ if code == 404 and (
902
+ "Unable to find item" in text
903
+ or "Not Found" in text
904
+ or "File not found" in text
905
+ ):
881
906
raise OSError (2 , f"No such file or directory: { url } " )
882
907
883
908
raise_for_status (response )
@@ -1479,6 +1504,21 @@ class PureArtifactoryPath(pathlib.PurePath):
1479
1504
_flavour = _artifactory_flavour
1480
1505
__slots__ = ()
1481
1506
1507
+ def _init (self , * args ):
1508
+ super ()._init (* args )
1509
+
1510
+ @classmethod
1511
+ def _split_root (cls , part ):
1512
+ cls ._flavour .splitroot (part )
1513
+
1514
+ @classmethod
1515
+ def _parse_parts (cls , parts ):
1516
+ return super ()._parse_parts (parts )
1517
+
1518
+ @classmethod
1519
+ def _format_parsed_parts (cls , drv , root , tail ):
1520
+ return super ()._format_parsed_parts (drv , root , tail )
1521
+
1482
1522
1483
1523
class _FakePathTemplate (object ):
1484
1524
def __init__ (self , accessor ):
@@ -1513,7 +1553,11 @@ def __new__(cls, *args, **kwargs):
1513
1553
So we have to first construct ArtifactoryPath by Pathlib and
1514
1554
only then add auth information.
1515
1555
"""
1556
+
1516
1557
obj = pathlib .Path .__new__ (cls , * args , ** kwargs )
1558
+ if _IS_PYTHON_3_12_OR_NEWER :
1559
+ # After python 3.12, all this logic can be moved to __init__
1560
+ return obj
1517
1561
1518
1562
cfg_entry = get_global_config_entry (obj .drive )
1519
1563
@@ -1565,6 +1609,56 @@ def _init(self, *args, **kwargs):
1565
1609
1566
1610
super (ArtifactoryPath , self )._init (* args , ** kwargs )
1567
1611
1612
+ def __init__ (self , * args , ** kwargs ):
1613
+ # Up until python3.12, pathlib.Path was not designed to be initialized
1614
+ # through __init__, so all that logic is in the __new__ method.
1615
+ if not _IS_PYTHON_3_12_OR_NEWER :
1616
+ return
1617
+
1618
+ super ().__init__ (* args , ** kwargs )
1619
+
1620
+ cfg_entry = get_global_config_entry (self .drive )
1621
+
1622
+ # Auth section
1623
+ apikey = kwargs .get ("apikey" )
1624
+ token = kwargs .get ("token" )
1625
+ auth_type = kwargs .get ("auth_type" )
1626
+
1627
+ if apikey :
1628
+ logger .debug ("Use XJFrogApiAuth apikey" )
1629
+ self .auth = XJFrogArtApiAuth (apikey = apikey )
1630
+ elif token :
1631
+ logger .debug ("Use XJFrogArtBearerAuth token" )
1632
+ self .auth = XJFrogArtBearerAuth (token = token )
1633
+ else :
1634
+ auth = kwargs .get ("auth" )
1635
+ self .auth = auth if auth_type is None else auth_type (* auth )
1636
+
1637
+ if self .auth is None and cfg_entry :
1638
+ auth = (cfg_entry ["username" ], cfg_entry ["password" ])
1639
+ self .auth = auth if auth_type is None else auth_type (* auth )
1640
+
1641
+ self .cert = kwargs .get ("cert" )
1642
+ self .session = kwargs .get ("session" )
1643
+ self .timeout = kwargs .get ("timeout" )
1644
+
1645
+ if self .cert is None and cfg_entry :
1646
+ self .cert = cfg_entry ["cert" ]
1647
+
1648
+ if "verify" in kwargs :
1649
+ self .verify = kwargs .get ("verify" )
1650
+ elif cfg_entry :
1651
+ self .verify = cfg_entry ["verify" ]
1652
+ else :
1653
+ self .verify = True
1654
+
1655
+ if self .session is None :
1656
+ self .session = requests .Session ()
1657
+ self .session .auth = self .auth
1658
+ self .session .cert = self .cert
1659
+ self .session .verify = self .verify
1660
+ self .session .timeout = self .timeout
1661
+
1568
1662
def __reduce__ (self ):
1569
1663
# pathlib.PurePath.__reduce__ doesn't include instance state, but we
1570
1664
# have state that needs to be included when pickling
@@ -1635,6 +1729,16 @@ def stat(self, pathobj=None):
1635
1729
pathobj = pathobj or self
1636
1730
return self ._accessor .stat (pathobj = pathobj )
1637
1731
1732
+ def exists (self ):
1733
+ try :
1734
+ self .stat ()
1735
+ except OSError :
1736
+ return False
1737
+ except ValueError :
1738
+ # Non-encodable path
1739
+ return False
1740
+ return True
1741
+
1638
1742
def mkdir (self , mode = 0o777 , parents = False , exist_ok = False ):
1639
1743
"""
1640
1744
Create a new directory at this given path.
@@ -2367,12 +2471,12 @@ def promote_docker_image(
2367
2471
2368
2472
@property
2369
2473
def repo (self ):
2370
- return self ._root . replace ( "/" , "" )
2474
+ return self .parts [ 1 ]
2371
2475
2372
2476
@property
2373
2477
def path_in_repo (self ):
2374
2478
parts = self .parts
2375
- path_in_repo = "/" + "/" .join (parts [1 :])
2479
+ path_in_repo = "/" + "/" .join (parts [2 :])
2376
2480
return path_in_repo
2377
2481
2378
2482
def find_user (self , name ):
0 commit comments