immlib 1.0.0.dev2__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 (45) hide show
  1. immlib/__init__.py +131 -0
  2. immlib/_init.py +108 -0
  3. immlib/_version.py +235 -0
  4. immlib/doc/__init__.py +38 -0
  5. immlib/doc/_core.py +311 -0
  6. immlib/iolib/__init__.py +29 -0
  7. immlib/iolib/_core.py +720 -0
  8. immlib/pathlib/__init__.py +69 -0
  9. immlib/pathlib/_cache.py +152 -0
  10. immlib/pathlib/_core.py +869 -0
  11. immlib/pathlib/_osf.py +538 -0
  12. immlib/test/__init__.py +16 -0
  13. immlib/test/__main__.py +10 -0
  14. immlib/test/doc/__init__.py +6 -0
  15. immlib/test/doc/test_core.py +91 -0
  16. immlib/test/iolib/__init__.py +7 -0
  17. immlib/test/iolib/test_core.py +81 -0
  18. immlib/test/pathlib/__init__.py +11 -0
  19. immlib/test/pathlib/test_core.py +146 -0
  20. immlib/test/pathlib/test_osf.py +54 -0
  21. immlib/test/types/__init__.py +5 -0
  22. immlib/test/types/test_core.py +110 -0
  23. immlib/test/util/__init__.py +11 -0
  24. immlib/test/util/test_core.py +681 -0
  25. immlib/test/util/test_numeric.py +1374 -0
  26. immlib/test/util/test_quantity.py +218 -0
  27. immlib/test/util/test_url.py +51 -0
  28. immlib/test/workflow/__init__.py +9 -0
  29. immlib/test/workflow/test_core.py +418 -0
  30. immlib/test/workflow/test_plantype.py +248 -0
  31. immlib/types/__init__.py +29 -0
  32. immlib/types/_core.py +333 -0
  33. immlib/util/__init__.py +283 -0
  34. immlib/util/_core.py +2524 -0
  35. immlib/util/_numeric.py +2651 -0
  36. immlib/util/_quantity.py +523 -0
  37. immlib/util/_url.py +114 -0
  38. immlib/workflow/__init__.py +48 -0
  39. immlib/workflow/_core.py +1635 -0
  40. immlib/workflow/_plantype.py +334 -0
  41. immlib-1.0.0.dev2.dist-info/METADATA +76 -0
  42. immlib-1.0.0.dev2.dist-info/RECORD +45 -0
  43. immlib-1.0.0.dev2.dist-info/WHEEL +5 -0
  44. immlib-1.0.0.dev2.dist-info/licenses/LICENSE +21 -0
  45. immlib-1.0.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,869 @@
1
+ # -*- coding: utf-8 -*-
2
+ ###############################################################################
3
+ # immlib/pathlib/_core.py
4
+
5
+ # Dependencies ################################################################
6
+
7
+ from platform import platform
8
+ from urllib.parse import urlparse
9
+ from collections import namedtuple
10
+ from pathlib import (Path, PurePath)
11
+ from os import (PathLike, fspath)
12
+
13
+ from pcollections import (ldict, lazy)
14
+ from cloudpathlib import (
15
+ CloudPath,
16
+ S3Path, AzureBlobPath, GSPath,
17
+ S3Client, AzureBlobClient, GSClient)
18
+
19
+ from ..doc import docwrap
20
+ from ..util import (is_str, is_amap, is_aseq, strstarts, strends)
21
+
22
+ from ._osf import (OSFClient, OSFPath)
23
+ from ._cache import CloudCachePath
24
+
25
+
26
+ # Global Values ###############################################################
27
+
28
+ # The set of drives, if we are in windows; this is used by the path logic to
29
+ # automatically determine if a path like 'c:/a/b/c' is a valid path.
30
+ windows_drives = None
31
+ if 'windows' in platform().lower():
32
+ from pathlib import WindowsPath
33
+ from ctypes import cdll
34
+ _ord_a = ord('a')
35
+ def is_windows_drive(letter):
36
+ try:
37
+ logical_drives = cdll.kernel32.GetLogicalDrives()
38
+ bitno_letter = ord(letter.lower()) - _ord_a
39
+ return bool((logical_drives >> bitno_letter) & 0x01)
40
+ except Exception:
41
+ return False
42
+ else:
43
+ def is_windows_drive(letter):
44
+ return False
45
+ is_windows_drive.__doc__ = \
46
+ """Determines whether a given string represents a valid Windows drive.
47
+
48
+ On non-windows platforms, ``is_windows_drive`` always returns ``False``. It
49
+ is additionally, possible that ``is_windows_drive`` will always return
50
+ false for certain Windows systems. The function requires the the
51
+ ``ctypes.cdll.kernel32`` object contain the ``GetLogicalDrives`` function
52
+ and that the substring 'windows' appear in the ``platform`` package's
53
+ ``platform.platform().lower()`` string.
54
+
55
+ If the above requirements are met, then ``is_windows_drive(letter)`` will
56
+ return ``True`` is ``letter`` is the letter of a valid drive and returns
57
+ ``False`` otherwise. Case is ignored.
58
+ """
59
+
60
+
61
+ # General Utilities ###########################################################
62
+
63
+ @docwrap
64
+ def pathstr(obj):
65
+ """Returns a string or bytes representation of a path.
66
+
67
+ ``pathstr(obj)`` returns ``obj`` itself if ``obj`` is either a ``str`` or
68
+ ``bytes`` object. If ``obj`` is a ``CloudPath`` object, then ``str(obj)``
69
+ is returned. Otherwise, If ``obj`` is a ``PathLike`` object, then
70
+ ``os.fspath(obj)`` is returned.
71
+ """
72
+ return str(obj) if isinstance(obj, CloudPath) else fspath(obj)
73
+
74
+
75
+ # OSFPath Functions ###########################################################
76
+
77
+ @docwrap
78
+ def is_osfpath(obj):
79
+ """Detects whether the input is an ``OSFPath`` object.
80
+
81
+ ``is_osfpath(obj)`` returns ``True`` if ``obj`` is an instance of the
82
+ ``OSFPath`` class and ``False`` otherwise.
83
+
84
+ See also: ``like_osfpath``
85
+
86
+ Parameters
87
+ ----------
88
+ obj : object
89
+ The object whose membership in the ``OSFPath`` class is to be
90
+ determined.
91
+
92
+ Returns
93
+ -------
94
+ boolean
95
+ ``True`` if ``obj`` is an instance of ``OSFPath`` and ``False``
96
+ otherwise.
97
+ """
98
+ return isinstance(obj, OSFPath)
99
+ @docwrap
100
+ def like_osfpath(obj):
101
+ """Detects whether the input can be converted into an ``OSFPath`` object.
102
+
103
+ ``like_osfpath(obj)`` returns ``True`` if ``obj`` is an instance of the
104
+ ``OSFPath`` class or is a string that forms a valid OSF path, and ``False``
105
+ otherwise.
106
+
107
+ See also: ``is_osfpath``
108
+
109
+ Parameters
110
+ ----------
111
+ obj : object
112
+ The object whose ability to be converted into an ``OSFPath`` instance
113
+ is to be determined.
114
+
115
+ Returns
116
+ -------
117
+ boolean
118
+ ``True`` if ``obj`` is an instance of ``OSFPath`` or is a string that
119
+ could be converted into an ``OSFPath`` and ``False`` otherwise.
120
+ """
121
+ if isinstance(obj, OSFPath):
122
+ return True
123
+ url = urlparse(pathstr(obj))
124
+ return bool(url.scheme == 'osf' and url.netloc)
125
+ @docwrap
126
+ def osfpath(obj, *args,
127
+ client=None,
128
+ cache_path=Ellipsis,
129
+ file_cache_mode=Ellipsis,
130
+ mkdir_mode=Ellipsis,
131
+ pagesize=Ellipsis,
132
+ local_cache_dir=None):
133
+ """Creates and returns an ``OSFPath`` representing an OSF.io repository.
134
+
135
+ ``osfpath(p)`` creates and returns an ``OSFPath`` object, which is a type
136
+ of ``cloudpathlib.CloudPath`` object, from the path or path-string
137
+ ``p``. If ``p`` is an ``OSFPath``, then it is returned as-is. Otherwise
138
+ ``str(p)`` is converted into an ``OSFPath``; ``str(p)`` may start with
139
+ ``'osf://'`` (not case-sensitive) or, if it does not have a scheme
140
+ specifier, ``'osf://'`` will be prepended to it.
141
+
142
+ ``osfpath(p, a1, a2...)`` converts ``p`` into an ``OSFPath`` then joins the
143
+ ``a1``, ``a2``, etc. values to the end of the path and returns the joined
144
+ path.
145
+
146
+ OSF paths take the format ``'osf://<project-ID>:<storage>/<path>'`` where
147
+ the storage is optional (defaulting to ``'osfstorage'``) and an empty path
148
+ refers to the project storage's root. The project-ID is the code used to
149
+ find the project online. For example, the webpage reached at the website
150
+ ``https://osf.io/tery8/`` is the project page for the project whose ID is
151
+ ``tery8``.
152
+
153
+ If any of the optional keyword arguments are given, then the returned path
154
+ will always use the specified options; a new path is returned with updated
155
+ options if necessary; this is done before joining paths if multiple
156
+ positional arguments are given. If the ``client`` keyword is given, then it
157
+ is modified by the keyword options before being used in the path. All
158
+ optional keyword arguments have a default value of ``Ellipsis``, which
159
+ indicates that the value of the ``client`` for that option should be used.
160
+
161
+ Parameters
162
+ ----------
163
+ obj : path-like
164
+ The path or path-like object to convert into an OSFPath, typically a
165
+ string.
166
+ client : OSFClient or None, optional
167
+ The ``OSFClient`` object to use. The ``OSFClient`` is responsible
168
+ primarily for the caching of data locally. If ``OSFClient`` is
169
+ ``None``, then an ``OSFClient`` object is created for the project using
170
+ a temporary cache directory.
171
+ cache_path : path-like or None, optional
172
+ The local directory in which cache files should be stored. This option
173
+ is ignored if ``client`` is not ``None``; otherwise it is passed to the
174
+ created client object. The cache directory is the root cache directory
175
+ for the entire OSF project.
176
+ file_cache_mode : cloudpathlib.enums.FileCacheMode, optional
177
+ How often to clear the file cache; see [cloudpathlib's caching
178
+ docs](https://cloudpathlib.drivendata.org/stable/caching/) for more
179
+ information about the options in ``cloudpathlib.enums.FileCacheMode``.
180
+ mkdir_mode : int, optional
181
+ The mode to use when making directories in the cache. By default this
182
+ is ``0o775``. This option is ignored if the ``client`` option is not
183
+ ``None``.
184
+ pagesize : int, optional
185
+ The number of items to include in a single page when paging directory
186
+ contents from the OSF server. The default is 100. This option is
187
+ ignored if the ``client`` option is not ``None``.
188
+ """
189
+ if isinstance(obj, OSFPath):
190
+ # We may want to grab some options out of the argument in this case.
191
+ if client is None:
192
+ client = obj.client
193
+ else:
194
+ # If there's no '://' in the path, then we add 'osf://' to the front.
195
+ obj = pathstr(obj)
196
+ if scheme_sep not in obj:
197
+ obj = 'osf://' + obj
198
+ # If the cache_path is Ellipsis and there's no client provided, then we
199
+ # want to use pimm's default cache path
200
+ if local_cache_dir is None:
201
+ if client is None:
202
+ if cache_path is Ellipsis:
203
+ cache_path = None #TODO
204
+ if cache_path is not None:
205
+ local_cache_dir = Path(cache_path).expanduser() / 'osf'
206
+ # At this point we can go ahead and create the initial path.
207
+ path = OSFPath(obj,
208
+ client=client,
209
+ local_cache_dir=local_cache_dir,
210
+ file_cache_mode=file_cache_mode,
211
+ mkdir_mode=mkdir_mode,
212
+ pagesize=pagesize)
213
+ # If there were additional arguments, append them to the path now.
214
+ for arg in args:
215
+ path = path / arg
216
+ # Return the path.
217
+ return path
218
+
219
+
220
+ # S3Path Functions ############################################################
221
+
222
+ @docwrap
223
+ def is_s3path(obj):
224
+ """Detects whether the input is an ``S3Path`` object.
225
+
226
+ ``is_s3path(obj)`` returns ``True`` if ``obj`` is an instance of the
227
+ ``S3Path`` class and ``False`` otherwise.
228
+
229
+ See also: ``like_s3path``
230
+
231
+ Parameters
232
+ ----------
233
+ obj : object
234
+ The object whose membership in the ``S3Path`` class is to be
235
+ determined.
236
+
237
+ Returns
238
+ -------
239
+ boolean
240
+ ``True`` if ``obj`` is an instance of ``S3Path`` and ``False``
241
+ otherwise.
242
+ """
243
+ return isinstance(obj, S3Path)
244
+ @docwrap
245
+ def like_s3path(obj):
246
+ """Detects whether the input can be converted into an ``S3Path`` object.
247
+
248
+ ``like_s3path(obj)`` returns ``True`` if ``obj`` is an instance of the
249
+ ``S3Path`` class or is a string that forms a valid S3 path, and ``False``
250
+ otherwise.
251
+
252
+ See also: ``is_s3path``
253
+
254
+ Parameters
255
+ ----------
256
+ obj : object
257
+ The object whose ability to be converted into an ``S3Path`` instance is
258
+ to be determined.
259
+
260
+ Returns
261
+ -------
262
+ boolean
263
+ ``True`` if ``obj`` is an instance of ``S3Path`` or is a string that
264
+ could be converted into an ``S3Path`` and ``False`` otherwise.
265
+ """
266
+ if isinstance(obj, S3Path):
267
+ return True
268
+ url = urlparse(pathstr(obj))
269
+ return bool(url.scheme == 's3' and url.netloc)
270
+ @docwrap
271
+ def s3path(obj, *args, **kwargs):
272
+ """Creates and returns an ``S3Path`` representing an AWS S3 repository.
273
+
274
+ ``s3path(p)`` creates and returns an ``S3Path`` object, which is a type of
275
+ ``cloudpathlib.CloudPath`` object, from the path or path-string ``p``. If
276
+ ``p`` is an ``S3Path``, then it is returned as-is. Otherwise ``pathstr(p)``
277
+ is converted into an ``S3Path``; ``pathstr(p)`` may start with ``'s3://'``
278
+ (not case-sensitive) or, if it does not have a scheme specifier,
279
+ ``'s3://'`` will be prepended to it.
280
+
281
+ ``s3path(p, a1, a2...)`` converts ``p`` into an ``S3Path`` then joins the
282
+ ``a1``, ``a2``, etc. values to the end of the path and returns the joined
283
+ path.
284
+
285
+ The ``s3path`` function accepts all the optional arguments of the
286
+ ``S3Client`` type from ``cloudpathlib`` as well as the ``client``
287
+ option. If the ``client`` option is given along with additional optional
288
+ arguments, then the optional arguments are ignored.
289
+
290
+ Additionally, ``s3path`` parses the option ``cache_path``, which is not
291
+ normally accepted by ``S3Path``, which instead requires the option
292
+ ``local_cache_dir``. Any time that a ``local_cache_dir`` is given, it
293
+ overrides the ``cache_path``; however, if ``local_cache_dir`` is not given
294
+ and ``cache_path`` is, then the directory ``os.path.join(cache_path,
295
+ "s3")`` is given as the ``local_cache_dir`` option.
296
+ """
297
+ # Extract a few options.
298
+ client = kwargs.pop('client', None)
299
+ cache_path = kwargs.pop('cache_path', None)
300
+ lcd = kwargs.get('local_cache_dir', None)
301
+ # Do some initial configuration.
302
+ if isinstance(obj, S3Path):
303
+ # We may want to grab some options out of the argument in this case.
304
+ if client is None:
305
+ client = obj.client
306
+ else:
307
+ # If there's no '://' in the path, then we add 's3://' to the front.
308
+ obj = pathstr(obj)
309
+ if scheme_sep not in obj:
310
+ obj = 's3://' + obj
311
+ if client is None:
312
+ # If the cache_path is Ellipsis and there's no client provided, then we
313
+ # want to use pimm's default cache path.
314
+ if cache_path is Ellipsis and client is None:
315
+ cache_path = None #TODO
316
+ if cache_path is not None and 'local_cache_dir' not in kwargs:
317
+ kwargs['local_cache_dir'] = Path(cache_path).expanduser() / "s3"
318
+ # At this point we can go ahead and create the client object.
319
+ client = S3Client(**kwargs)
320
+ path = S3Path(obj, client=client)
321
+ # If there were additional arguments, append them to the path now.
322
+ for arg in args:
323
+ path = path / arg
324
+ # Return the path.
325
+ return path
326
+
327
+
328
+ # GSPath Functions ############################################################
329
+
330
+ @docwrap
331
+ def is_gspath(obj):
332
+ """Detects whether the input is an ``GSPath`` object.
333
+
334
+ ``is_gspath(obj)`` returns ``True`` if `obj` is an instance of the
335
+ ``GSPath`` class and ``False`` otherwise.
336
+
337
+ See also: ``like_gspath``
338
+
339
+ Parameters
340
+ ----------
341
+ obj : object
342
+ The object whose membership in the ``GSPath`` class is to be
343
+ determined.
344
+
345
+ Returns
346
+ -------
347
+ boolean
348
+ ``True`` if `obj` is an instance of ``GSPath`` and ``False``
349
+ otherwise.
350
+
351
+ """
352
+ return isinstance(obj, GSPath)
353
+ @docwrap
354
+ def like_gspath(obj):
355
+ """Detects whether the input can be converted into an ``GSPath`` object.
356
+
357
+ ``like_gspath(obj)`` returns ``True`` if `obj` is an instance of the
358
+ ``GSPath`` class or is a string that forms a valid GS path, and ``False``
359
+ otherwise.
360
+
361
+ See also: ``is_gspath``
362
+
363
+ Parameters
364
+ ----------
365
+ obj : object
366
+ The object whose ability to be converted into an ``GSPath`` instance is
367
+ to be determined.
368
+
369
+ Returns
370
+ -------
371
+ boolean
372
+ ``True`` if `obj` is an instance of ``GSPath`` or is a string that
373
+ could be converted into an ``GSPath`` and ``False`` otherwise.
374
+ """
375
+ if isinstance(obj, GSPath):
376
+ return True
377
+ url = urlparse(pathstr(obj))
378
+ return bool(url.scheme == 'gs' and url.netloc)
379
+ @docwrap
380
+ def gspath(obj, *args, **kwargs):
381
+ """Creates and returns an ``GSPath`` representing a Google Storage
382
+ repository.
383
+
384
+ ``gspath(p)`` creates and returns a ``GSPath`` object, which is a type of
385
+ ``cloudpathlib.CloudPath`` object, from the path or path-string ``p``. If
386
+ ``p`` is a ``GSPath``, then it is returned as-is. Otherwise ``pathstr(p)``
387
+ is converted into an ``GSPath``; ``pathstr(p)`` may start with ``'gs://'``
388
+ (not case-sensitive) or, if it does not have a scheme specifier,
389
+ ``'gs://'`` will be prepended to it.
390
+
391
+ ``gspath(p, a1, a2...)`` converts ``p`` into an ``GSPath`` then joins the
392
+ ``a1``, ``a2``, etc. values to the end of the path and returns the joined
393
+ path.
394
+
395
+ The ``gspath`` function accepts all the optional arguments of the
396
+ ``GSClient`` type from ``cloudpathlib`` as well as the ``client``
397
+ option. If the ``client`` option is given along with additional optional
398
+ arguments, then the optional arguments are ignored.
399
+
400
+ Additionally, ``gspath`` parses the option ``cache_path``, which is not
401
+ normally accepted by ``GSPath``, which instead requires the option
402
+ ``local_cache_dir``. Any time that a ``local_cache_dir`` is given, it
403
+ overrides the ``cache_path``; however, if ``local_cache_dir`` is not given
404
+ and ``cache_path`` is, then the directory ``os.path.join(cache_path,
405
+ "gs")`` is given as the ``local_cache_dir`` option.
406
+ """
407
+ # Extract a few options.
408
+ client = kwargs.pop('client', None)
409
+ cache_path = kwargs.pop('cache_path', None)
410
+ lcd = kwargs.get('local_cache_dir', None)
411
+ # Do some initial configuration.
412
+ if isinstance(obj, GSPath):
413
+ # We may want to grab some options out of the argument in this case.
414
+ if client is None:
415
+ client = obj.client
416
+ else:
417
+ # If there's no '://' in the path, then we add 'osf://' to the front.
418
+ obj = pathstr(obj)
419
+ if scheme_sep not in obj:
420
+ obj = 'gs://' + obj
421
+ # At this point we can go ahead and create the client object.
422
+ if client is None:
423
+ # If the cache_path is Ellipsis and there's no client provided, then we
424
+ # want to use pimm's default cache path.
425
+ if cache_path is Ellipsis and client is None:
426
+ cache_path = None #TODO
427
+ if cache_path is not None and 'local_cache_dir' not in kwargs:
428
+ kwargs['local_cache_dir'] = Path(cache_path).expanduser() / "gs"
429
+ client = GSClient(**kwargs)
430
+ path = GSPath(obj, client=client)
431
+ # If there were additional arguments, append them to the path now.
432
+ for arg in args:
433
+ path = path / arg
434
+ # Return the path.
435
+ return path
436
+
437
+
438
+ # AzureBlobPath Functions #####################################################
439
+
440
+ @docwrap
441
+ def is_azpath(obj):
442
+ """Detects whether the input is an ``AzureBlobPath`` object.
443
+
444
+ ``is_azpath(obj)`` returns ``True`` if `obj` is an instance of the
445
+ ``AzureBlobPath`` class and ``False`` otherwise.
446
+
447
+ See also: ``like_azpath``
448
+
449
+ Parameters
450
+ ----------
451
+ obj : object
452
+ The object whose membership in the ``AzureBlobPath`` class is to be
453
+ determined.
454
+
455
+ Returns
456
+ -------
457
+ boolean
458
+ ``True`` if `obj` is an instance of ``AzureBlobPath`` and ``False``
459
+ otherwise.
460
+ """
461
+ return isinstance(obj, AzureBlobPath)
462
+ @docwrap
463
+ def like_azpath(obj):
464
+ """Detects whether an input can be converted into an ``AzureBlobPath``
465
+ object.
466
+
467
+ ``like_azpath(obj)`` returns ``True`` if `obj` is an instance of the
468
+ ``AzureBlobPath`` class or is a string that forms a valid Azure path, and
469
+ ``False`` otherwise.
470
+
471
+ See also: ``is_azpath``
472
+
473
+ Parameters
474
+ ----------
475
+ obj : object
476
+ The object whose ability to be converted into an ``AzureBlobPath``
477
+ instance is to be determined.
478
+
479
+ Returns
480
+ -------
481
+ boolean
482
+ ``True`` if `obj` is an instance of ``AzureBlobPath`` or is a string
483
+ that could be converted into an ``AzureBlobPath`` and ``False``
484
+ otherwise.
485
+ """
486
+ if isinstance(obj, AzureBlobPath):
487
+ return True
488
+ url = urlparse(pathstr(obj))
489
+ return bool(url.scheme == 'az' and url.netloc)
490
+ @docwrap
491
+ def azpath(obj, *args, **kwargs):
492
+ """Creates and returns an ``AzureBlobPath`` representing an Azure
493
+ repository.
494
+
495
+ ``azpath(p)`` creates and returns an ``AzureBlobPath`` object, which is a
496
+ type of ``cloudpathlib.CloudPath`` object, from the path or path-string
497
+ ``p``. If ``p`` is an ``AzureBlobPath``, then it is returned
498
+ as-is. Otherwise ``pathstr(p)`` is converted into an ``AzureBlobPath``;
499
+ ``pathstr(p)`` may start with ``'az://'`` (not case-sensitive) or, if it
500
+ does not have a scheme specifier, ``'az://'`` will be prepended to it.
501
+
502
+ ``azpath(p, a1, a2...)`` converts ``p`` into an ``AzureBlobPath`` then
503
+ joins the ``a1``, ``a2``, etc. values to the end of the path and returns
504
+ the joined path.
505
+
506
+ The ``azpath`` function accepts all the optional arguments of the
507
+ ``AzureBlobClient`` type from ``cloudpathlib`` as well as the ``client``
508
+ option. If the ``client`` option is given along with additional optional
509
+ arguments, then the optional arguments are ignored.
510
+
511
+ Additionally, ``azpath`` parses the option ``cache_path``, which is not
512
+ normally accepted by ``AzureBlobPath``, which instead requires the option
513
+ ``local_cache_dir``. Any time that a ``local_cache_dir`` is given, it
514
+ overrides the ``cache_path``; however, if ``local_cache_dir`` is not given
515
+ and ``cache_path`` is, then the directory ``os.path.join(cache_path,"az")``
516
+ is given as the ``local_cache_dir`` option.
517
+ """
518
+ # Extract a few options.
519
+ client = kwargs.pop('client', None)
520
+ cache_path = kwargs.pop('cache_path', None)
521
+ lcd = kwargs.get('local_cache_dir', None)
522
+ # Do some initial configuration.
523
+ if isinstance(obj, AzureBlobPath):
524
+ # We may want to grab some options out of the argument in this case.
525
+ if client is None:
526
+ client = obj.client
527
+ else:
528
+ # If there's no '://' in the path, then we add 'osf://' to the front.
529
+ obj = pathstr(obj)
530
+ if scheme_sep not in obj:
531
+ obj = 'az://' + obj
532
+ if client is None:
533
+ # If the cache_path is Ellipsis and there's no client provided, then we
534
+ # want to use pimm's default cache path.
535
+ if cache_path is Ellipsis and client is None:
536
+ cache_path = None #TODO
537
+ if cache_path is not None and 'local_cache_dir' not in kwargs:
538
+ kwargs['local_cache_dir'] = Path(cache_path).expanduser() / "az"
539
+ # At this point we can go ahead and create the client object.
540
+ client = AzureBlobClient(**kwargs)
541
+ path = AzureBlobPath(obj, client=client)
542
+ # If there were additional arguments, append them to the path now.
543
+ for arg in args:
544
+ path = path / arg
545
+ # Return the path.
546
+ return path
547
+
548
+
549
+ # Filesystem Paths ############################################################
550
+
551
+ @docwrap
552
+ def is_filepath(p):
553
+ """Detects whether an object is a filesystem ``Path`` object.
554
+
555
+ Any object that inherits from the ``Path`` type is considered a filesystem
556
+ path. File-paths correspond to URLs that begin with ``'file://'``.
557
+
558
+ Parameters
559
+ ----------
560
+ p : path-like
561
+ The object whose quality as a path is to be assessed.
562
+
563
+ Returns
564
+ -------
565
+ boolean
566
+ ``True`` if `p` is an instance of the ``Path`` type and ``False``
567
+ otherwise.
568
+ """
569
+ return isinstance(p, Path)
570
+ @docwrap
571
+ def like_filepath(obj):
572
+ """Detects whether an input can be converted into a ``Path`` object.
573
+
574
+ ``like_filepath(obj)`` returns ``True`` if `obj` is an instance of the
575
+ ``Path`` class or is a string that forms a valid path, and ``False``
576
+ otherwise. Most strings are at least theoretically valid paths, but any
577
+ string that starts with a sheme followed by ``'://'`` must have the
578
+ ``'file'`` scheme.
579
+
580
+ See also: ``is_filepath``
581
+
582
+ Parameters
583
+ ----------
584
+ obj : object
585
+ The object whose ability to be converted into a ``Path`` instance is to
586
+ be determined.
587
+
588
+ Returns
589
+ -------
590
+ boolean
591
+ ``True`` if `obj` is an instance of ``AzureBlobPath`` or is a string
592
+ that could be converted into an ``AzureBlobPath`` and ``False``
593
+ otherwise.
594
+ """
595
+ if isinstance(obj, Path):
596
+ return True
597
+ url = urlparse(pathstr(obj))
598
+ return bool(url.scheme == '' or url.scheme == 'file')
599
+ @docwrap
600
+ def filepath(p, *args):
601
+ """Returns a local ``Path`` object for the given path if possible.
602
+
603
+ The ``filepath`` function is intended to coerce remote paths (such as the
604
+ S3 or OSF paths managed through the ``cloudpathlib.CloudPath`` class) into
605
+ paths representing their local caches. If a local file is requested, then
606
+ it is always downloaded before the ``Path`` is returned. For directories,
607
+ the cache directory itself will always exist, but no such guarantee is made
608
+ about its contents.
609
+
610
+ If the argument to ``filepath`` is a string and not a path object, then
611
+ it is converted into a path via the ``immlib.path`` function.
612
+
613
+ Parameters
614
+ ----------
615
+ p : path-like
616
+ The object whose local cache path is to be returned after it has been
617
+ downloaded.
618
+
619
+ Returns
620
+ -------
621
+ Path
622
+ If the input path `p` is already a local path, then it is returned. If
623
+ `p` is a remote path, then it is downloaded and its cache path is
624
+ returned. If there is no local cache or if the file does not exist, an
625
+ error is raised.
626
+ """
627
+ # We are converting the argument p into a Path of some kind.
628
+ if isinstance(p, CloudPath):
629
+ # If p is a CloudPath, we return a path that represents the cache.
630
+ return CloudCachePath(p)
631
+ if isinstance(p, (PathLike, PurePath)):
632
+ # If p is a path, str, or PathLike, we can just return a copy.
633
+ return Path(p)
634
+ elif isinstance(p, str):
635
+ # We need to make sure that it doesn't start with file:// before we
636
+ # pass this string along.
637
+ if p.startswith('file://'):
638
+ p = p[7:]
639
+ return Path(p)
640
+ elif isinstance(p, bytes):
641
+ # Same as above but with bytes.
642
+ if p.startswith(b'file://'):
643
+ p = p[7:]
644
+ return Path(p)
645
+ else:
646
+ # Try to convert it to a pathstr then turn it into a path.
647
+ return Path(pathstr(p))
648
+
649
+
650
+ # Other Utility Functions #####################################################
651
+
652
+ # The registry of all recognized path types uses a named tuple to store
653
+ # entries. Pure paths are not included as they are innate to the functions.
654
+ PathTypeRecord = namedtuple(
655
+ 'PathTypeRecord',
656
+ ('construct', 'isfn', 'likefn', 'fspath'))
657
+ def _cloudtofspath(cp):
658
+ return cp.fspath
659
+ pathtypes = dict(
660
+ s3=PathTypeRecord(s3path, is_s3path, like_s3path, _cloudtofspath),
661
+ gs=PathTypeRecord(gspath, is_gspath, like_gspath, _cloudtofspath),
662
+ az=PathTypeRecord(azpath, is_azpath, like_azpath, _cloudtofspath),
663
+ osf=PathTypeRecord(osfpath, is_osfpath, like_osfpath, _cloudtofspath),
664
+ file=PathTypeRecord(
665
+ filepath, is_filepath, like_filepath, lambda path:path))
666
+ scheme_sep = '://'
667
+ @docwrap
668
+ def pathtype(path, default='file', encoding='utf-8'):
669
+ """Given a path, returns the pathtype record and remaining path.
670
+
671
+ This is an internal function that looks up the pathtype for a path from the
672
+ ``pathtypes`` dictionary and returning both the pathtype record for the
673
+ path.
674
+
675
+ Paths that do not have an explicit scheme marker are considered filesystem
676
+ paths.
677
+
678
+ Parameters
679
+ ----------
680
+ path : path-like or str
681
+ The path whose pathtype record is being extracted.
682
+ default : object, optional
683
+ The scheme that is assumed if no scheme is specified (i.e.,
684
+ ``str(path)`` does not begin with ``'<scheme>://'``. By default this is
685
+ ``'file'``.
686
+ encoding : str, optional
687
+ The encoding to use if the path argument is a ``bytes`` object; the
688
+ default is ``'utf-8'``.
689
+
690
+ Returns
691
+ -------
692
+ PathTypeRecord
693
+ A data record about the path type of the given path.
694
+
695
+ Raises
696
+ ------
697
+ KeyError
698
+ If the scheme of the the given path is not recognized.
699
+ """
700
+ if isinstance(path, bytes):
701
+ path = path.decode('utf-8')
702
+ spath = pathstr(path)
703
+ if scheme_sep not in spath:
704
+ scheme = default
705
+ else:
706
+ scheme = spath.split(scheme_sep)[0]
707
+ return pathtypes[scheme]
708
+ @docwrap
709
+ def is_path(p):
710
+ """Detects whether an object is either a ``Path`` or a ``CloudPath``
711
+ object.
712
+
713
+ Both ``Path`` and ``CloudPath`` objects abstractly represent paths, but
714
+ they do not share a subclass. ``is_path`` tests whether an object's type is
715
+ a subclass of any of path types recognized by ``immlib``. Additional path
716
+ types can be registered by adding ``immlib.paths.PathTypeRecord`` instances
717
+ to the ``immlib.pathtypes`` dictionary. The key for such a record should be
718
+ the string prefix for the path type (such as ``'s3'`` for an ``S3Path``
719
+ type).
720
+
721
+ Parameters
722
+ ----------
723
+ p : path-like
724
+ The object whose quality as a path is to be assessed.
725
+
726
+ Returns
727
+ -------
728
+ boolean
729
+ ``True`` if `p` has a type that is recognized by ``immlib`` as a path
730
+ type and ``False`` otherwise.
731
+ """
732
+ try:
733
+ pt = pathtype(p)
734
+ except (KeyError, TypeError):
735
+ return False
736
+ return pt.isfn(p)
737
+ @docwrap
738
+ def like_path(p):
739
+ """Detects whether an object is either like a ``Path`` or ``CloudPath``
740
+ object.
741
+
742
+ Both ``Path`` and ``CloudPath`` object abstractly represent paths, but they
743
+ do not share a subclass. ``like_path`` tests whether an object's type is a
744
+ subclass of any of path types recognized by ``immlib`` or is a string or
745
+ bytes object that could be converted into a path. Additional path types can
746
+ be registered by adding ``immlib.pathlib.PathTypeRecord`` instances to the
747
+ ``immlib.pathlib.pathtypes`` dictionary. The key for such a record should
748
+ be the string prefix for the path type (such as ``'s3'`` for an ``S3Path``
749
+ type).
750
+
751
+ Parameters
752
+ ----------
753
+ p : object
754
+ The object whose quality as a path-like object is to be assessed.
755
+
756
+ Returns
757
+ -------
758
+ boolean
759
+ ``True`` if `p` has a type that is recognized by ``immlib`` as a path
760
+ type or is an object that can be converted into a path type and
761
+ ``False`` otherwise.
762
+ """
763
+ try:
764
+ pt = pathtype(p)
765
+ except KeyError:
766
+ return False
767
+ return pt.likefn(p)
768
+
769
+
770
+ # path ########################################################################
771
+
772
+ @docwrap
773
+ def path(arg0, *args, **kwargs):
774
+ """Convenience function for instantiating ``Path`` objects.
775
+
776
+ ``path(arg)`` returns a ``Path``-like object that references the path given
777
+ by the argument ``arg``. The ``arg`` is converted into a string prior to
778
+ conversion into a path if it is not a path already.
779
+
780
+ ``path(arg, *args)`` joins the list of arguments in ``args`` to the path
781
+ created from the ``arg``.
782
+
783
+ Optional keyword arguments may be given as well,
784
+ """
785
+ nargs = len(args)
786
+ # What kind of pathtype is this?
787
+ try:
788
+ pt = pathtype(arg0)
789
+ except KeyError:
790
+ pt = None
791
+ if pt is None:
792
+ raise ValueError(f"unrecognized pathtype for path: {arg0}")
793
+ # Start by converting the first argument into a path, if it isn't one.
794
+ if not kwargs and pt.isfn(arg0):
795
+ # We only need to update the path if new keyword args were given.
796
+ p = arg0
797
+ else:
798
+ # We have a pathtype, so we can create a path from these arguments.
799
+ p = pt.construct(arg0, **kwargs)
800
+ # At this point, p is a pathtype, so we can append any extra args.
801
+ # Join the rest of the arguments to the path.
802
+ for arg in args:
803
+ p = p / arg
804
+ # That is all that's required.
805
+ return p
806
+
807
+ @docwrap
808
+ def pathdict(arg, all=False, filter=None, ondir=None, onfile=None):
809
+ """Returns a lazy dictionary of the nested paths beginning at the argument.
810
+
811
+ Searches a directory and all its subdirectories, lazily, for all contents,
812
+ uniting them in a single nested lazy dictionary. The dictionaries in the
813
+ nest represent directories whose keys are the filenames of their
814
+ contents. The value of a key is another lazy dictionaries or a path object
815
+ if the file is not a directory.
816
+
817
+ Parameters
818
+ ----------
819
+ arg : path-like
820
+ The path from which to begin the search.
821
+ all : boolean, optional
822
+ Whether to include hidden files (``True``) or not (``False``) in the
823
+ directory lists. The default is ``False``.
824
+ filter : None or function, optional
825
+ A filter that must return either ``True`` (indicating that the path
826
+ should be included in the pathdict) or ``False`` (indicating that the
827
+ path should not be included in the pathdict) for each path that is
828
+ scanned. The default is ``None``, meaning that no filter is applied.
829
+ ondir : function, optional
830
+ A function to run on any path encountered during the ``pathdict``
831
+ search that is a directory. When ``pathdict`` is given a path that is a
832
+ directory, it returns a lazy dictionary whose keys are the filenames of
833
+ the contents of that directory. For subdirectories, their filenames are
834
+ mapped to the return value of ``ondir(path)`` where ``path`` is the
835
+ path object for the subdirectory. By default this is ``pathdict``
836
+ itself, resulting in a nested structure for subdirectories. The
837
+ ``all``, ``filter``, ``ondir``, and ``onfile`` parameters are all
838
+ passed to this function.
839
+ onfile : function, optional
840
+ A function to run on any path encountered during the ``pathdict``
841
+ search that is a file. When ``pathdict`` is given a path that is a
842
+ directory, it returns a lazy dictionary whose keys are the filenames of
843
+ the contents of that directory. For files in the directory, their
844
+ filenames are mapped to the return value of ``onfile(path)`` where
845
+ ``path`` is the path object for the file. By default this is ``None``,
846
+ indicating that the path itself should be returned.
847
+
848
+ Returns
849
+ -------
850
+ ldict
851
+ A lazy dictionary of the contents of the argument.
852
+ """
853
+ if ondir is None:
854
+ ondir = pathdict
855
+ root = path(arg)
856
+ if root.is_dir():
857
+ contents = {}
858
+ kw = {'all':all, 'filter':filter, 'ondir':ondir, 'onfile':onfile}
859
+ for p in root.iterdir():
860
+ if p.is_file():
861
+ q = p if onfile is None else lazy(onfile, p)
862
+ else:
863
+ q = p if ondir is None else lazy(ondir, p, **kw)
864
+ contents[p.name] = q
865
+ return ldict(contents)
866
+ elif onfile is None:
867
+ return root
868
+ else:
869
+ return onfile(root)