Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +4 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_asyncio.cpython-38-x86_64-linux-gnu.so +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_ctypes.cpython-38-x86_64-linux-gnu.so +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_socket.cpython-38-x86_64-linux-gnu.so +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/lib2to3/Grammar3.8.18.final.0.pickle +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/__pycache__/autopep8.cpython-38.pyc +3 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/__init__.py +24 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/_common.py +43 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/_version.py +4 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/easter.py +89 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/relativedelta.py +599 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/rrule.py +1737 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/tzwin.py +2 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/utils.py +71 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/__init__.py +65 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/_version.py +21 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/archive.py +68 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/asyn.py +933 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/caching.py +521 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/callbacks.py +238 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/compression.py +173 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/config.py +99 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/conftest.py +55 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/core.py +707 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/dircache.py +96 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/exceptions.py +21 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/fuse.py +324 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/generic.py +167 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/gui.py +408 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/mapping.py +232 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/parquet.py +551 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/registry.py +270 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/spec.py +1697 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/transaction.py +81 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/utils.py +621 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/LICENSE.txt +19 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/METADATA +107 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/RECORD +416 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/REQUESTED +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/WHEEL +5 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/top_level.txt +4 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/__init__.py +23 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/compose.py +62 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/errors.py +45 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/initialize.py +179 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/main.py +100 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/py.typed +0 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/types.py +82 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/utils.py +84 -0
- my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/version.py +80 -0
.gitattributes
CHANGED
|
@@ -255,3 +255,7 @@ my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_ssl.cpython-
|
|
| 255 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_testbuffer.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 256 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_sqlite3.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 257 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/tkinter/__pycache__/__init__.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_testbuffer.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 256 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_sqlite3.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 257 |
my_container_sandbox/workspace/anaconda3/lib/python3.8/tkinter/__pycache__/__init__.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 258 |
+
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_socket.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 259 |
+
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_ctypes.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 260 |
+
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/__pycache__/autopep8.cpython-38.pyc filter=lfs diff=lfs merge=lfs -text
|
| 261 |
+
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_asyncio.cpython-38-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_asyncio.cpython-38-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7805a83a6295ab6cccb70abde69a7ac28d986b943f490387cb7948ff8033bb36
|
| 3 |
+
size 225344
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_ctypes.cpython-38-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fbef76d31c5ef234af2c4686fc45de37fc51bb4ade60c77a893179db581ad297
|
| 3 |
+
size 513728
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib-dynload/_socket.cpython-38-x86_64-linux-gnu.so
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f5cd0ce9e6b42fc744a208bde0d6c23947cae12cd94493072414d85cd38a86d
|
| 3 |
+
size 283768
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/lib2to3/Grammar3.8.18.final.0.pickle
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3131a0354ebd272fdb9f419a5045a4e814b8d28b4482f7ba2874eb6a1d4fc228
|
| 3 |
+
size 15309
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/__pycache__/autopep8.cpython-38.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae195c0994a3df4625694e123f01da6ad6437932d70ddf7659a391631ae1e611
|
| 3 |
+
size 100765
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
try:
|
| 5 |
+
from ._version import version as __version__
|
| 6 |
+
except ImportError:
|
| 7 |
+
__version__ = 'unknown'
|
| 8 |
+
|
| 9 |
+
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
| 10 |
+
'utils', 'zoneinfo']
|
| 11 |
+
|
| 12 |
+
def __getattr__(name):
|
| 13 |
+
import importlib
|
| 14 |
+
|
| 15 |
+
if name in __all__:
|
| 16 |
+
return importlib.import_module("." + name, __name__)
|
| 17 |
+
raise AttributeError(
|
| 18 |
+
"module {!r} has not attribute {!r}".format(__name__, name)
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def __dir__():
|
| 23 |
+
# __dir__ should include all the lazy-importable modules as well.
|
| 24 |
+
return [x for x in globals() if x not in sys.modules] + __all__
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/_common.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Common code used in multiple modules.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class weekday(object):
|
| 7 |
+
__slots__ = ["weekday", "n"]
|
| 8 |
+
|
| 9 |
+
def __init__(self, weekday, n=None):
|
| 10 |
+
self.weekday = weekday
|
| 11 |
+
self.n = n
|
| 12 |
+
|
| 13 |
+
def __call__(self, n):
|
| 14 |
+
if n == self.n:
|
| 15 |
+
return self
|
| 16 |
+
else:
|
| 17 |
+
return self.__class__(self.weekday, n)
|
| 18 |
+
|
| 19 |
+
def __eq__(self, other):
|
| 20 |
+
try:
|
| 21 |
+
if self.weekday != other.weekday or self.n != other.n:
|
| 22 |
+
return False
|
| 23 |
+
except AttributeError:
|
| 24 |
+
return False
|
| 25 |
+
return True
|
| 26 |
+
|
| 27 |
+
def __hash__(self):
|
| 28 |
+
return hash((
|
| 29 |
+
self.weekday,
|
| 30 |
+
self.n,
|
| 31 |
+
))
|
| 32 |
+
|
| 33 |
+
def __ne__(self, other):
|
| 34 |
+
return not (self == other)
|
| 35 |
+
|
| 36 |
+
def __repr__(self):
|
| 37 |
+
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
| 38 |
+
if not self.n:
|
| 39 |
+
return s
|
| 40 |
+
else:
|
| 41 |
+
return "%s(%+d)" % (s, self.n)
|
| 42 |
+
|
| 43 |
+
# vim:ts=4:sw=4:et
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/_version.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# file generated by setuptools_scm
|
| 2 |
+
# don't change, don't track in version control
|
| 3 |
+
__version__ = version = '2.9.0.post0'
|
| 4 |
+
__version_tuple__ = version_tuple = (2, 9, 0)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/easter.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers a generic Easter computing method for any given year, using
|
| 4 |
+
Western, Orthodox or Julian algorithms.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import datetime
|
| 8 |
+
|
| 9 |
+
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
| 10 |
+
|
| 11 |
+
EASTER_JULIAN = 1
|
| 12 |
+
EASTER_ORTHODOX = 2
|
| 13 |
+
EASTER_WESTERN = 3
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def easter(year, method=EASTER_WESTERN):
|
| 17 |
+
"""
|
| 18 |
+
This method was ported from the work done by GM Arts,
|
| 19 |
+
on top of the algorithm by Claus Tondering, which was
|
| 20 |
+
based in part on the algorithm of Ouding (1940), as
|
| 21 |
+
quoted in "Explanatory Supplement to the Astronomical
|
| 22 |
+
Almanac", P. Kenneth Seidelmann, editor.
|
| 23 |
+
|
| 24 |
+
This algorithm implements three different Easter
|
| 25 |
+
calculation methods:
|
| 26 |
+
|
| 27 |
+
1. Original calculation in Julian calendar, valid in
|
| 28 |
+
dates after 326 AD
|
| 29 |
+
2. Original method, with date converted to Gregorian
|
| 30 |
+
calendar, valid in years 1583 to 4099
|
| 31 |
+
3. Revised method, in Gregorian calendar, valid in
|
| 32 |
+
years 1583 to 4099 as well
|
| 33 |
+
|
| 34 |
+
These methods are represented by the constants:
|
| 35 |
+
|
| 36 |
+
* ``EASTER_JULIAN = 1``
|
| 37 |
+
* ``EASTER_ORTHODOX = 2``
|
| 38 |
+
* ``EASTER_WESTERN = 3``
|
| 39 |
+
|
| 40 |
+
The default method is method 3.
|
| 41 |
+
|
| 42 |
+
More about the algorithm may be found at:
|
| 43 |
+
|
| 44 |
+
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
| 45 |
+
|
| 46 |
+
and
|
| 47 |
+
|
| 48 |
+
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
| 49 |
+
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
if not (1 <= method <= 3):
|
| 53 |
+
raise ValueError("invalid method")
|
| 54 |
+
|
| 55 |
+
# g - Golden year - 1
|
| 56 |
+
# c - Century
|
| 57 |
+
# h - (23 - Epact) mod 30
|
| 58 |
+
# i - Number of days from March 21 to Paschal Full Moon
|
| 59 |
+
# j - Weekday for PFM (0=Sunday, etc)
|
| 60 |
+
# p - Number of days from March 21 to Sunday on or before PFM
|
| 61 |
+
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
| 62 |
+
# e - Extra days to add for method 2 (converting Julian
|
| 63 |
+
# date to Gregorian date)
|
| 64 |
+
|
| 65 |
+
y = year
|
| 66 |
+
g = y % 19
|
| 67 |
+
e = 0
|
| 68 |
+
if method < 3:
|
| 69 |
+
# Old method
|
| 70 |
+
i = (19*g + 15) % 30
|
| 71 |
+
j = (y + y//4 + i) % 7
|
| 72 |
+
if method == 2:
|
| 73 |
+
# Extra dates to convert Julian to Gregorian date
|
| 74 |
+
e = 10
|
| 75 |
+
if y > 1600:
|
| 76 |
+
e = e + y//100 - 16 - (y//100 - 16)//4
|
| 77 |
+
else:
|
| 78 |
+
# New method
|
| 79 |
+
c = y//100
|
| 80 |
+
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
| 81 |
+
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
| 82 |
+
j = (y + y//4 + i + 2 - c + c//4) % 7
|
| 83 |
+
|
| 84 |
+
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
| 85 |
+
# (later dates apply to method 2, although 23 May never actually occurs)
|
| 86 |
+
p = i - j + e
|
| 87 |
+
d = 1 + (p + 27 + (p + 6)//40) % 31
|
| 88 |
+
m = 3 + (p + 26)//30
|
| 89 |
+
return datetime.date(int(y), int(m), int(d))
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/relativedelta.py
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import datetime
|
| 3 |
+
import calendar
|
| 4 |
+
|
| 5 |
+
import operator
|
| 6 |
+
from math import copysign
|
| 7 |
+
|
| 8 |
+
from six import integer_types
|
| 9 |
+
from warnings import warn
|
| 10 |
+
|
| 11 |
+
from ._common import weekday
|
| 12 |
+
|
| 13 |
+
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
| 14 |
+
|
| 15 |
+
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class relativedelta(object):
|
| 19 |
+
"""
|
| 20 |
+
The relativedelta type is designed to be applied to an existing datetime and
|
| 21 |
+
can replace specific components of that datetime, or represents an interval
|
| 22 |
+
of time.
|
| 23 |
+
|
| 24 |
+
It is based on the specification of the excellent work done by M.-A. Lemburg
|
| 25 |
+
in his
|
| 26 |
+
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
| 27 |
+
However, notice that this type does *NOT* implement the same algorithm as
|
| 28 |
+
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
| 29 |
+
|
| 30 |
+
There are two different ways to build a relativedelta instance. The
|
| 31 |
+
first one is passing it two date/datetime classes::
|
| 32 |
+
|
| 33 |
+
relativedelta(datetime1, datetime2)
|
| 34 |
+
|
| 35 |
+
The second one is passing it any number of the following keyword arguments::
|
| 36 |
+
|
| 37 |
+
relativedelta(arg1=x,arg2=y,arg3=z...)
|
| 38 |
+
|
| 39 |
+
year, month, day, hour, minute, second, microsecond:
|
| 40 |
+
Absolute information (argument is singular); adding or subtracting a
|
| 41 |
+
relativedelta with absolute information does not perform an arithmetic
|
| 42 |
+
operation, but rather REPLACES the corresponding value in the
|
| 43 |
+
original datetime with the value(s) in relativedelta.
|
| 44 |
+
|
| 45 |
+
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
| 46 |
+
Relative information, may be negative (argument is plural); adding
|
| 47 |
+
or subtracting a relativedelta with relative information performs
|
| 48 |
+
the corresponding arithmetic operation on the original datetime value
|
| 49 |
+
with the information in the relativedelta.
|
| 50 |
+
|
| 51 |
+
weekday:
|
| 52 |
+
One of the weekday instances (MO, TU, etc) available in the
|
| 53 |
+
relativedelta module. These instances may receive a parameter N,
|
| 54 |
+
specifying the Nth weekday, which could be positive or negative
|
| 55 |
+
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
| 56 |
+
+1. You can also use an integer, where 0=MO. This argument is always
|
| 57 |
+
relative e.g. if the calculated date is already Monday, using MO(1)
|
| 58 |
+
or MO(-1) won't change the day. To effectively make it absolute, use
|
| 59 |
+
it in combination with the day argument (e.g. day=1, MO(1) for first
|
| 60 |
+
Monday of the month).
|
| 61 |
+
|
| 62 |
+
leapdays:
|
| 63 |
+
Will add given days to the date found, if year is a leap
|
| 64 |
+
year, and the date found is post 28 of february.
|
| 65 |
+
|
| 66 |
+
yearday, nlyearday:
|
| 67 |
+
Set the yearday or the non-leap year day (jump leap days).
|
| 68 |
+
These are converted to day/month/leapdays information.
|
| 69 |
+
|
| 70 |
+
There are relative and absolute forms of the keyword
|
| 71 |
+
arguments. The plural is relative, and the singular is
|
| 72 |
+
absolute. For each argument in the order below, the absolute form
|
| 73 |
+
is applied first (by setting each attribute to that value) and
|
| 74 |
+
then the relative form (by adding the value to the attribute).
|
| 75 |
+
|
| 76 |
+
The order of attributes considered when this relativedelta is
|
| 77 |
+
added to a datetime is:
|
| 78 |
+
|
| 79 |
+
1. Year
|
| 80 |
+
2. Month
|
| 81 |
+
3. Day
|
| 82 |
+
4. Hours
|
| 83 |
+
5. Minutes
|
| 84 |
+
6. Seconds
|
| 85 |
+
7. Microseconds
|
| 86 |
+
|
| 87 |
+
Finally, weekday is applied, using the rule described above.
|
| 88 |
+
|
| 89 |
+
For example
|
| 90 |
+
|
| 91 |
+
>>> from datetime import datetime
|
| 92 |
+
>>> from dateutil.relativedelta import relativedelta, MO
|
| 93 |
+
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
| 94 |
+
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
| 95 |
+
>>> dt + delta
|
| 96 |
+
datetime.datetime(2018, 4, 2, 14, 37)
|
| 97 |
+
|
| 98 |
+
First, the day is set to 1 (the first of the month), then 25 hours
|
| 99 |
+
are added, to get to the 2nd day and 14th hour, finally the
|
| 100 |
+
weekday is applied, but since the 2nd is already a Monday there is
|
| 101 |
+
no effect.
|
| 102 |
+
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def __init__(self, dt1=None, dt2=None,
|
| 106 |
+
years=0, months=0, days=0, leapdays=0, weeks=0,
|
| 107 |
+
hours=0, minutes=0, seconds=0, microseconds=0,
|
| 108 |
+
year=None, month=None, day=None, weekday=None,
|
| 109 |
+
yearday=None, nlyearday=None,
|
| 110 |
+
hour=None, minute=None, second=None, microsecond=None):
|
| 111 |
+
|
| 112 |
+
if dt1 and dt2:
|
| 113 |
+
# datetime is a subclass of date. So both must be date
|
| 114 |
+
if not (isinstance(dt1, datetime.date) and
|
| 115 |
+
isinstance(dt2, datetime.date)):
|
| 116 |
+
raise TypeError("relativedelta only diffs datetime/date")
|
| 117 |
+
|
| 118 |
+
# We allow two dates, or two datetimes, so we coerce them to be
|
| 119 |
+
# of the same type
|
| 120 |
+
if (isinstance(dt1, datetime.datetime) !=
|
| 121 |
+
isinstance(dt2, datetime.datetime)):
|
| 122 |
+
if not isinstance(dt1, datetime.datetime):
|
| 123 |
+
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
| 124 |
+
elif not isinstance(dt2, datetime.datetime):
|
| 125 |
+
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
| 126 |
+
|
| 127 |
+
self.years = 0
|
| 128 |
+
self.months = 0
|
| 129 |
+
self.days = 0
|
| 130 |
+
self.leapdays = 0
|
| 131 |
+
self.hours = 0
|
| 132 |
+
self.minutes = 0
|
| 133 |
+
self.seconds = 0
|
| 134 |
+
self.microseconds = 0
|
| 135 |
+
self.year = None
|
| 136 |
+
self.month = None
|
| 137 |
+
self.day = None
|
| 138 |
+
self.weekday = None
|
| 139 |
+
self.hour = None
|
| 140 |
+
self.minute = None
|
| 141 |
+
self.second = None
|
| 142 |
+
self.microsecond = None
|
| 143 |
+
self._has_time = 0
|
| 144 |
+
|
| 145 |
+
# Get year / month delta between the two
|
| 146 |
+
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
| 147 |
+
self._set_months(months)
|
| 148 |
+
|
| 149 |
+
# Remove the year/month delta so the timedelta is just well-defined
|
| 150 |
+
# time units (seconds, days and microseconds)
|
| 151 |
+
dtm = self.__radd__(dt2)
|
| 152 |
+
|
| 153 |
+
# If we've overshot our target, make an adjustment
|
| 154 |
+
if dt1 < dt2:
|
| 155 |
+
compare = operator.gt
|
| 156 |
+
increment = 1
|
| 157 |
+
else:
|
| 158 |
+
compare = operator.lt
|
| 159 |
+
increment = -1
|
| 160 |
+
|
| 161 |
+
while compare(dt1, dtm):
|
| 162 |
+
months += increment
|
| 163 |
+
self._set_months(months)
|
| 164 |
+
dtm = self.__radd__(dt2)
|
| 165 |
+
|
| 166 |
+
# Get the timedelta between the "months-adjusted" date and dt1
|
| 167 |
+
delta = dt1 - dtm
|
| 168 |
+
self.seconds = delta.seconds + delta.days * 86400
|
| 169 |
+
self.microseconds = delta.microseconds
|
| 170 |
+
else:
|
| 171 |
+
# Check for non-integer values in integer-only quantities
|
| 172 |
+
if any(x is not None and x != int(x) for x in (years, months)):
|
| 173 |
+
raise ValueError("Non-integer years and months are "
|
| 174 |
+
"ambiguous and not currently supported.")
|
| 175 |
+
|
| 176 |
+
# Relative information
|
| 177 |
+
self.years = int(years)
|
| 178 |
+
self.months = int(months)
|
| 179 |
+
self.days = days + weeks * 7
|
| 180 |
+
self.leapdays = leapdays
|
| 181 |
+
self.hours = hours
|
| 182 |
+
self.minutes = minutes
|
| 183 |
+
self.seconds = seconds
|
| 184 |
+
self.microseconds = microseconds
|
| 185 |
+
|
| 186 |
+
# Absolute information
|
| 187 |
+
self.year = year
|
| 188 |
+
self.month = month
|
| 189 |
+
self.day = day
|
| 190 |
+
self.hour = hour
|
| 191 |
+
self.minute = minute
|
| 192 |
+
self.second = second
|
| 193 |
+
self.microsecond = microsecond
|
| 194 |
+
|
| 195 |
+
if any(x is not None and int(x) != x
|
| 196 |
+
for x in (year, month, day, hour,
|
| 197 |
+
minute, second, microsecond)):
|
| 198 |
+
# For now we'll deprecate floats - later it'll be an error.
|
| 199 |
+
warn("Non-integer value passed as absolute information. " +
|
| 200 |
+
"This is not a well-defined condition and will raise " +
|
| 201 |
+
"errors in future versions.", DeprecationWarning)
|
| 202 |
+
|
| 203 |
+
if isinstance(weekday, integer_types):
|
| 204 |
+
self.weekday = weekdays[weekday]
|
| 205 |
+
else:
|
| 206 |
+
self.weekday = weekday
|
| 207 |
+
|
| 208 |
+
yday = 0
|
| 209 |
+
if nlyearday:
|
| 210 |
+
yday = nlyearday
|
| 211 |
+
elif yearday:
|
| 212 |
+
yday = yearday
|
| 213 |
+
if yearday > 59:
|
| 214 |
+
self.leapdays = -1
|
| 215 |
+
if yday:
|
| 216 |
+
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
| 217 |
+
243, 273, 304, 334, 366]
|
| 218 |
+
for idx, ydays in enumerate(ydayidx):
|
| 219 |
+
if yday <= ydays:
|
| 220 |
+
self.month = idx+1
|
| 221 |
+
if idx == 0:
|
| 222 |
+
self.day = yday
|
| 223 |
+
else:
|
| 224 |
+
self.day = yday-ydayidx[idx-1]
|
| 225 |
+
break
|
| 226 |
+
else:
|
| 227 |
+
raise ValueError("invalid year day (%d)" % yday)
|
| 228 |
+
|
| 229 |
+
self._fix()
|
| 230 |
+
|
| 231 |
+
def _fix(self):
|
| 232 |
+
if abs(self.microseconds) > 999999:
|
| 233 |
+
s = _sign(self.microseconds)
|
| 234 |
+
div, mod = divmod(self.microseconds * s, 1000000)
|
| 235 |
+
self.microseconds = mod * s
|
| 236 |
+
self.seconds += div * s
|
| 237 |
+
if abs(self.seconds) > 59:
|
| 238 |
+
s = _sign(self.seconds)
|
| 239 |
+
div, mod = divmod(self.seconds * s, 60)
|
| 240 |
+
self.seconds = mod * s
|
| 241 |
+
self.minutes += div * s
|
| 242 |
+
if abs(self.minutes) > 59:
|
| 243 |
+
s = _sign(self.minutes)
|
| 244 |
+
div, mod = divmod(self.minutes * s, 60)
|
| 245 |
+
self.minutes = mod * s
|
| 246 |
+
self.hours += div * s
|
| 247 |
+
if abs(self.hours) > 23:
|
| 248 |
+
s = _sign(self.hours)
|
| 249 |
+
div, mod = divmod(self.hours * s, 24)
|
| 250 |
+
self.hours = mod * s
|
| 251 |
+
self.days += div * s
|
| 252 |
+
if abs(self.months) > 11:
|
| 253 |
+
s = _sign(self.months)
|
| 254 |
+
div, mod = divmod(self.months * s, 12)
|
| 255 |
+
self.months = mod * s
|
| 256 |
+
self.years += div * s
|
| 257 |
+
if (self.hours or self.minutes or self.seconds or self.microseconds
|
| 258 |
+
or self.hour is not None or self.minute is not None or
|
| 259 |
+
self.second is not None or self.microsecond is not None):
|
| 260 |
+
self._has_time = 1
|
| 261 |
+
else:
|
| 262 |
+
self._has_time = 0
|
| 263 |
+
|
| 264 |
+
@property
|
| 265 |
+
def weeks(self):
|
| 266 |
+
return int(self.days / 7.0)
|
| 267 |
+
|
| 268 |
+
@weeks.setter
|
| 269 |
+
def weeks(self, value):
|
| 270 |
+
self.days = self.days - (self.weeks * 7) + value * 7
|
| 271 |
+
|
| 272 |
+
def _set_months(self, months):
|
| 273 |
+
self.months = months
|
| 274 |
+
if abs(self.months) > 11:
|
| 275 |
+
s = _sign(self.months)
|
| 276 |
+
div, mod = divmod(self.months * s, 12)
|
| 277 |
+
self.months = mod * s
|
| 278 |
+
self.years = div * s
|
| 279 |
+
else:
|
| 280 |
+
self.years = 0
|
| 281 |
+
|
| 282 |
+
def normalized(self):
|
| 283 |
+
"""
|
| 284 |
+
Return a version of this object represented entirely using integer
|
| 285 |
+
values for the relative attributes.
|
| 286 |
+
|
| 287 |
+
>>> relativedelta(days=1.5, hours=2).normalized()
|
| 288 |
+
relativedelta(days=+1, hours=+14)
|
| 289 |
+
|
| 290 |
+
:return:
|
| 291 |
+
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
| 292 |
+
"""
|
| 293 |
+
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
| 294 |
+
days = int(self.days)
|
| 295 |
+
|
| 296 |
+
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
| 297 |
+
hours = int(hours_f)
|
| 298 |
+
|
| 299 |
+
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
| 300 |
+
minutes = int(minutes_f)
|
| 301 |
+
|
| 302 |
+
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
| 303 |
+
seconds = int(seconds_f)
|
| 304 |
+
|
| 305 |
+
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
| 306 |
+
|
| 307 |
+
# Constructor carries overflow back up with call to _fix()
|
| 308 |
+
return self.__class__(years=self.years, months=self.months,
|
| 309 |
+
days=days, hours=hours, minutes=minutes,
|
| 310 |
+
seconds=seconds, microseconds=microseconds,
|
| 311 |
+
leapdays=self.leapdays, year=self.year,
|
| 312 |
+
month=self.month, day=self.day,
|
| 313 |
+
weekday=self.weekday, hour=self.hour,
|
| 314 |
+
minute=self.minute, second=self.second,
|
| 315 |
+
microsecond=self.microsecond)
|
| 316 |
+
|
| 317 |
+
def __add__(self, other):
|
| 318 |
+
if isinstance(other, relativedelta):
|
| 319 |
+
return self.__class__(years=other.years + self.years,
|
| 320 |
+
months=other.months + self.months,
|
| 321 |
+
days=other.days + self.days,
|
| 322 |
+
hours=other.hours + self.hours,
|
| 323 |
+
minutes=other.minutes + self.minutes,
|
| 324 |
+
seconds=other.seconds + self.seconds,
|
| 325 |
+
microseconds=(other.microseconds +
|
| 326 |
+
self.microseconds),
|
| 327 |
+
leapdays=other.leapdays or self.leapdays,
|
| 328 |
+
year=(other.year if other.year is not None
|
| 329 |
+
else self.year),
|
| 330 |
+
month=(other.month if other.month is not None
|
| 331 |
+
else self.month),
|
| 332 |
+
day=(other.day if other.day is not None
|
| 333 |
+
else self.day),
|
| 334 |
+
weekday=(other.weekday if other.weekday is not None
|
| 335 |
+
else self.weekday),
|
| 336 |
+
hour=(other.hour if other.hour is not None
|
| 337 |
+
else self.hour),
|
| 338 |
+
minute=(other.minute if other.minute is not None
|
| 339 |
+
else self.minute),
|
| 340 |
+
second=(other.second if other.second is not None
|
| 341 |
+
else self.second),
|
| 342 |
+
microsecond=(other.microsecond if other.microsecond
|
| 343 |
+
is not None else
|
| 344 |
+
self.microsecond))
|
| 345 |
+
if isinstance(other, datetime.timedelta):
|
| 346 |
+
return self.__class__(years=self.years,
|
| 347 |
+
months=self.months,
|
| 348 |
+
days=self.days + other.days,
|
| 349 |
+
hours=self.hours,
|
| 350 |
+
minutes=self.minutes,
|
| 351 |
+
seconds=self.seconds + other.seconds,
|
| 352 |
+
microseconds=self.microseconds + other.microseconds,
|
| 353 |
+
leapdays=self.leapdays,
|
| 354 |
+
year=self.year,
|
| 355 |
+
month=self.month,
|
| 356 |
+
day=self.day,
|
| 357 |
+
weekday=self.weekday,
|
| 358 |
+
hour=self.hour,
|
| 359 |
+
minute=self.minute,
|
| 360 |
+
second=self.second,
|
| 361 |
+
microsecond=self.microsecond)
|
| 362 |
+
if not isinstance(other, datetime.date):
|
| 363 |
+
return NotImplemented
|
| 364 |
+
elif self._has_time and not isinstance(other, datetime.datetime):
|
| 365 |
+
other = datetime.datetime.fromordinal(other.toordinal())
|
| 366 |
+
year = (self.year or other.year)+self.years
|
| 367 |
+
month = self.month or other.month
|
| 368 |
+
if self.months:
|
| 369 |
+
assert 1 <= abs(self.months) <= 12
|
| 370 |
+
month += self.months
|
| 371 |
+
if month > 12:
|
| 372 |
+
year += 1
|
| 373 |
+
month -= 12
|
| 374 |
+
elif month < 1:
|
| 375 |
+
year -= 1
|
| 376 |
+
month += 12
|
| 377 |
+
day = min(calendar.monthrange(year, month)[1],
|
| 378 |
+
self.day or other.day)
|
| 379 |
+
repl = {"year": year, "month": month, "day": day}
|
| 380 |
+
for attr in ["hour", "minute", "second", "microsecond"]:
|
| 381 |
+
value = getattr(self, attr)
|
| 382 |
+
if value is not None:
|
| 383 |
+
repl[attr] = value
|
| 384 |
+
days = self.days
|
| 385 |
+
if self.leapdays and month > 2 and calendar.isleap(year):
|
| 386 |
+
days += self.leapdays
|
| 387 |
+
ret = (other.replace(**repl)
|
| 388 |
+
+ datetime.timedelta(days=days,
|
| 389 |
+
hours=self.hours,
|
| 390 |
+
minutes=self.minutes,
|
| 391 |
+
seconds=self.seconds,
|
| 392 |
+
microseconds=self.microseconds))
|
| 393 |
+
if self.weekday:
|
| 394 |
+
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
| 395 |
+
jumpdays = (abs(nth) - 1) * 7
|
| 396 |
+
if nth > 0:
|
| 397 |
+
jumpdays += (7 - ret.weekday() + weekday) % 7
|
| 398 |
+
else:
|
| 399 |
+
jumpdays += (ret.weekday() - weekday) % 7
|
| 400 |
+
jumpdays *= -1
|
| 401 |
+
ret += datetime.timedelta(days=jumpdays)
|
| 402 |
+
return ret
|
| 403 |
+
|
| 404 |
+
def __radd__(self, other):
|
| 405 |
+
return self.__add__(other)
|
| 406 |
+
|
| 407 |
+
def __rsub__(self, other):
|
| 408 |
+
return self.__neg__().__radd__(other)
|
| 409 |
+
|
| 410 |
+
def __sub__(self, other):
|
| 411 |
+
if not isinstance(other, relativedelta):
|
| 412 |
+
return NotImplemented # In case the other object defines __rsub__
|
| 413 |
+
return self.__class__(years=self.years - other.years,
|
| 414 |
+
months=self.months - other.months,
|
| 415 |
+
days=self.days - other.days,
|
| 416 |
+
hours=self.hours - other.hours,
|
| 417 |
+
minutes=self.minutes - other.minutes,
|
| 418 |
+
seconds=self.seconds - other.seconds,
|
| 419 |
+
microseconds=self.microseconds - other.microseconds,
|
| 420 |
+
leapdays=self.leapdays or other.leapdays,
|
| 421 |
+
year=(self.year if self.year is not None
|
| 422 |
+
else other.year),
|
| 423 |
+
month=(self.month if self.month is not None else
|
| 424 |
+
other.month),
|
| 425 |
+
day=(self.day if self.day is not None else
|
| 426 |
+
other.day),
|
| 427 |
+
weekday=(self.weekday if self.weekday is not None else
|
| 428 |
+
other.weekday),
|
| 429 |
+
hour=(self.hour if self.hour is not None else
|
| 430 |
+
other.hour),
|
| 431 |
+
minute=(self.minute if self.minute is not None else
|
| 432 |
+
other.minute),
|
| 433 |
+
second=(self.second if self.second is not None else
|
| 434 |
+
other.second),
|
| 435 |
+
microsecond=(self.microsecond if self.microsecond
|
| 436 |
+
is not None else
|
| 437 |
+
other.microsecond))
|
| 438 |
+
|
| 439 |
+
def __abs__(self):
|
| 440 |
+
return self.__class__(years=abs(self.years),
|
| 441 |
+
months=abs(self.months),
|
| 442 |
+
days=abs(self.days),
|
| 443 |
+
hours=abs(self.hours),
|
| 444 |
+
minutes=abs(self.minutes),
|
| 445 |
+
seconds=abs(self.seconds),
|
| 446 |
+
microseconds=abs(self.microseconds),
|
| 447 |
+
leapdays=self.leapdays,
|
| 448 |
+
year=self.year,
|
| 449 |
+
month=self.month,
|
| 450 |
+
day=self.day,
|
| 451 |
+
weekday=self.weekday,
|
| 452 |
+
hour=self.hour,
|
| 453 |
+
minute=self.minute,
|
| 454 |
+
second=self.second,
|
| 455 |
+
microsecond=self.microsecond)
|
| 456 |
+
|
| 457 |
+
def __neg__(self):
|
| 458 |
+
return self.__class__(years=-self.years,
|
| 459 |
+
months=-self.months,
|
| 460 |
+
days=-self.days,
|
| 461 |
+
hours=-self.hours,
|
| 462 |
+
minutes=-self.minutes,
|
| 463 |
+
seconds=-self.seconds,
|
| 464 |
+
microseconds=-self.microseconds,
|
| 465 |
+
leapdays=self.leapdays,
|
| 466 |
+
year=self.year,
|
| 467 |
+
month=self.month,
|
| 468 |
+
day=self.day,
|
| 469 |
+
weekday=self.weekday,
|
| 470 |
+
hour=self.hour,
|
| 471 |
+
minute=self.minute,
|
| 472 |
+
second=self.second,
|
| 473 |
+
microsecond=self.microsecond)
|
| 474 |
+
|
| 475 |
+
def __bool__(self):
|
| 476 |
+
return not (not self.years and
|
| 477 |
+
not self.months and
|
| 478 |
+
not self.days and
|
| 479 |
+
not self.hours and
|
| 480 |
+
not self.minutes and
|
| 481 |
+
not self.seconds and
|
| 482 |
+
not self.microseconds and
|
| 483 |
+
not self.leapdays and
|
| 484 |
+
self.year is None and
|
| 485 |
+
self.month is None and
|
| 486 |
+
self.day is None and
|
| 487 |
+
self.weekday is None and
|
| 488 |
+
self.hour is None and
|
| 489 |
+
self.minute is None and
|
| 490 |
+
self.second is None and
|
| 491 |
+
self.microsecond is None)
|
| 492 |
+
# Compatibility with Python 2.x
|
| 493 |
+
__nonzero__ = __bool__
|
| 494 |
+
|
| 495 |
+
def __mul__(self, other):
|
| 496 |
+
try:
|
| 497 |
+
f = float(other)
|
| 498 |
+
except TypeError:
|
| 499 |
+
return NotImplemented
|
| 500 |
+
|
| 501 |
+
return self.__class__(years=int(self.years * f),
|
| 502 |
+
months=int(self.months * f),
|
| 503 |
+
days=int(self.days * f),
|
| 504 |
+
hours=int(self.hours * f),
|
| 505 |
+
minutes=int(self.minutes * f),
|
| 506 |
+
seconds=int(self.seconds * f),
|
| 507 |
+
microseconds=int(self.microseconds * f),
|
| 508 |
+
leapdays=self.leapdays,
|
| 509 |
+
year=self.year,
|
| 510 |
+
month=self.month,
|
| 511 |
+
day=self.day,
|
| 512 |
+
weekday=self.weekday,
|
| 513 |
+
hour=self.hour,
|
| 514 |
+
minute=self.minute,
|
| 515 |
+
second=self.second,
|
| 516 |
+
microsecond=self.microsecond)
|
| 517 |
+
|
| 518 |
+
__rmul__ = __mul__
|
| 519 |
+
|
| 520 |
+
def __eq__(self, other):
|
| 521 |
+
if not isinstance(other, relativedelta):
|
| 522 |
+
return NotImplemented
|
| 523 |
+
if self.weekday or other.weekday:
|
| 524 |
+
if not self.weekday or not other.weekday:
|
| 525 |
+
return False
|
| 526 |
+
if self.weekday.weekday != other.weekday.weekday:
|
| 527 |
+
return False
|
| 528 |
+
n1, n2 = self.weekday.n, other.weekday.n
|
| 529 |
+
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
| 530 |
+
return False
|
| 531 |
+
return (self.years == other.years and
|
| 532 |
+
self.months == other.months and
|
| 533 |
+
self.days == other.days and
|
| 534 |
+
self.hours == other.hours and
|
| 535 |
+
self.minutes == other.minutes and
|
| 536 |
+
self.seconds == other.seconds and
|
| 537 |
+
self.microseconds == other.microseconds and
|
| 538 |
+
self.leapdays == other.leapdays and
|
| 539 |
+
self.year == other.year and
|
| 540 |
+
self.month == other.month and
|
| 541 |
+
self.day == other.day and
|
| 542 |
+
self.hour == other.hour and
|
| 543 |
+
self.minute == other.minute and
|
| 544 |
+
self.second == other.second and
|
| 545 |
+
self.microsecond == other.microsecond)
|
| 546 |
+
|
| 547 |
+
def __hash__(self):
|
| 548 |
+
return hash((
|
| 549 |
+
self.weekday,
|
| 550 |
+
self.years,
|
| 551 |
+
self.months,
|
| 552 |
+
self.days,
|
| 553 |
+
self.hours,
|
| 554 |
+
self.minutes,
|
| 555 |
+
self.seconds,
|
| 556 |
+
self.microseconds,
|
| 557 |
+
self.leapdays,
|
| 558 |
+
self.year,
|
| 559 |
+
self.month,
|
| 560 |
+
self.day,
|
| 561 |
+
self.hour,
|
| 562 |
+
self.minute,
|
| 563 |
+
self.second,
|
| 564 |
+
self.microsecond,
|
| 565 |
+
))
|
| 566 |
+
|
| 567 |
+
def __ne__(self, other):
|
| 568 |
+
return not self.__eq__(other)
|
| 569 |
+
|
| 570 |
+
def __div__(self, other):
|
| 571 |
+
try:
|
| 572 |
+
reciprocal = 1 / float(other)
|
| 573 |
+
except TypeError:
|
| 574 |
+
return NotImplemented
|
| 575 |
+
|
| 576 |
+
return self.__mul__(reciprocal)
|
| 577 |
+
|
| 578 |
+
__truediv__ = __div__
|
| 579 |
+
|
| 580 |
+
def __repr__(self):
|
| 581 |
+
l = []
|
| 582 |
+
for attr in ["years", "months", "days", "leapdays",
|
| 583 |
+
"hours", "minutes", "seconds", "microseconds"]:
|
| 584 |
+
value = getattr(self, attr)
|
| 585 |
+
if value:
|
| 586 |
+
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
| 587 |
+
for attr in ["year", "month", "day", "weekday",
|
| 588 |
+
"hour", "minute", "second", "microsecond"]:
|
| 589 |
+
value = getattr(self, attr)
|
| 590 |
+
if value is not None:
|
| 591 |
+
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
| 592 |
+
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
| 593 |
+
attrs=", ".join(l))
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
def _sign(x):
|
| 597 |
+
return int(copysign(1, x))
|
| 598 |
+
|
| 599 |
+
# vim:ts=4:sw=4:et
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/rrule.py
ADDED
|
@@ -0,0 +1,1737 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
The rrule module offers a small, complete, and very fast, implementation of
|
| 4 |
+
the recurrence rules documented in the
|
| 5 |
+
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
| 6 |
+
including support for caching of results.
|
| 7 |
+
"""
|
| 8 |
+
import calendar
|
| 9 |
+
import datetime
|
| 10 |
+
import heapq
|
| 11 |
+
import itertools
|
| 12 |
+
import re
|
| 13 |
+
import sys
|
| 14 |
+
from functools import wraps
|
| 15 |
+
# For warning about deprecation of until and count
|
| 16 |
+
from warnings import warn
|
| 17 |
+
|
| 18 |
+
from six import advance_iterator, integer_types
|
| 19 |
+
|
| 20 |
+
from six.moves import _thread, range
|
| 21 |
+
|
| 22 |
+
from ._common import weekday as weekdaybase
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from math import gcd
|
| 26 |
+
except ImportError:
|
| 27 |
+
from fractions import gcd
|
| 28 |
+
|
| 29 |
+
__all__ = ["rrule", "rruleset", "rrulestr",
|
| 30 |
+
"YEARLY", "MONTHLY", "WEEKLY", "DAILY",
|
| 31 |
+
"HOURLY", "MINUTELY", "SECONDLY",
|
| 32 |
+
"MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
| 33 |
+
|
| 34 |
+
# Every mask is 7 days longer to handle cross-year weekly periods.
|
| 35 |
+
M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
|
| 36 |
+
[7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
|
| 37 |
+
M365MASK = list(M366MASK)
|
| 38 |
+
M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
|
| 39 |
+
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
| 40 |
+
MDAY365MASK = list(MDAY366MASK)
|
| 41 |
+
M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
|
| 42 |
+
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
| 43 |
+
NMDAY365MASK = list(NMDAY366MASK)
|
| 44 |
+
M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
|
| 45 |
+
M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
|
| 46 |
+
WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
|
| 47 |
+
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
|
| 48 |
+
MDAY365MASK = tuple(MDAY365MASK)
|
| 49 |
+
M365MASK = tuple(M365MASK)
|
| 50 |
+
|
| 51 |
+
FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
|
| 52 |
+
|
| 53 |
+
(YEARLY,
|
| 54 |
+
MONTHLY,
|
| 55 |
+
WEEKLY,
|
| 56 |
+
DAILY,
|
| 57 |
+
HOURLY,
|
| 58 |
+
MINUTELY,
|
| 59 |
+
SECONDLY) = list(range(7))
|
| 60 |
+
|
| 61 |
+
# Imported on demand.
|
| 62 |
+
easter = None
|
| 63 |
+
parser = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class weekday(weekdaybase):
|
| 67 |
+
"""
|
| 68 |
+
This version of weekday does not allow n = 0.
|
| 69 |
+
"""
|
| 70 |
+
def __init__(self, wkday, n=None):
|
| 71 |
+
if n == 0:
|
| 72 |
+
raise ValueError("Can't create weekday with n==0")
|
| 73 |
+
|
| 74 |
+
super(weekday, self).__init__(wkday, n)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _invalidates_cache(f):
|
| 81 |
+
"""
|
| 82 |
+
Decorator for rruleset methods which may invalidate the
|
| 83 |
+
cached length.
|
| 84 |
+
"""
|
| 85 |
+
@wraps(f)
|
| 86 |
+
def inner_func(self, *args, **kwargs):
|
| 87 |
+
rv = f(self, *args, **kwargs)
|
| 88 |
+
self._invalidate_cache()
|
| 89 |
+
return rv
|
| 90 |
+
|
| 91 |
+
return inner_func
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class rrulebase(object):
|
| 95 |
+
def __init__(self, cache=False):
|
| 96 |
+
if cache:
|
| 97 |
+
self._cache = []
|
| 98 |
+
self._cache_lock = _thread.allocate_lock()
|
| 99 |
+
self._invalidate_cache()
|
| 100 |
+
else:
|
| 101 |
+
self._cache = None
|
| 102 |
+
self._cache_complete = False
|
| 103 |
+
self._len = None
|
| 104 |
+
|
| 105 |
+
def __iter__(self):
|
| 106 |
+
if self._cache_complete:
|
| 107 |
+
return iter(self._cache)
|
| 108 |
+
elif self._cache is None:
|
| 109 |
+
return self._iter()
|
| 110 |
+
else:
|
| 111 |
+
return self._iter_cached()
|
| 112 |
+
|
| 113 |
+
def _invalidate_cache(self):
|
| 114 |
+
if self._cache is not None:
|
| 115 |
+
self._cache = []
|
| 116 |
+
self._cache_complete = False
|
| 117 |
+
self._cache_gen = self._iter()
|
| 118 |
+
|
| 119 |
+
if self._cache_lock.locked():
|
| 120 |
+
self._cache_lock.release()
|
| 121 |
+
|
| 122 |
+
self._len = None
|
| 123 |
+
|
| 124 |
+
def _iter_cached(self):
|
| 125 |
+
i = 0
|
| 126 |
+
gen = self._cache_gen
|
| 127 |
+
cache = self._cache
|
| 128 |
+
acquire = self._cache_lock.acquire
|
| 129 |
+
release = self._cache_lock.release
|
| 130 |
+
while gen:
|
| 131 |
+
if i == len(cache):
|
| 132 |
+
acquire()
|
| 133 |
+
if self._cache_complete:
|
| 134 |
+
break
|
| 135 |
+
try:
|
| 136 |
+
for j in range(10):
|
| 137 |
+
cache.append(advance_iterator(gen))
|
| 138 |
+
except StopIteration:
|
| 139 |
+
self._cache_gen = gen = None
|
| 140 |
+
self._cache_complete = True
|
| 141 |
+
break
|
| 142 |
+
release()
|
| 143 |
+
yield cache[i]
|
| 144 |
+
i += 1
|
| 145 |
+
while i < self._len:
|
| 146 |
+
yield cache[i]
|
| 147 |
+
i += 1
|
| 148 |
+
|
| 149 |
+
def __getitem__(self, item):
|
| 150 |
+
if self._cache_complete:
|
| 151 |
+
return self._cache[item]
|
| 152 |
+
elif isinstance(item, slice):
|
| 153 |
+
if item.step and item.step < 0:
|
| 154 |
+
return list(iter(self))[item]
|
| 155 |
+
else:
|
| 156 |
+
return list(itertools.islice(self,
|
| 157 |
+
item.start or 0,
|
| 158 |
+
item.stop or sys.maxsize,
|
| 159 |
+
item.step or 1))
|
| 160 |
+
elif item >= 0:
|
| 161 |
+
gen = iter(self)
|
| 162 |
+
try:
|
| 163 |
+
for i in range(item+1):
|
| 164 |
+
res = advance_iterator(gen)
|
| 165 |
+
except StopIteration:
|
| 166 |
+
raise IndexError
|
| 167 |
+
return res
|
| 168 |
+
else:
|
| 169 |
+
return list(iter(self))[item]
|
| 170 |
+
|
| 171 |
+
def __contains__(self, item):
|
| 172 |
+
if self._cache_complete:
|
| 173 |
+
return item in self._cache
|
| 174 |
+
else:
|
| 175 |
+
for i in self:
|
| 176 |
+
if i == item:
|
| 177 |
+
return True
|
| 178 |
+
elif i > item:
|
| 179 |
+
return False
|
| 180 |
+
return False
|
| 181 |
+
|
| 182 |
+
# __len__() introduces a large performance penalty.
|
| 183 |
+
def count(self):
|
| 184 |
+
""" Returns the number of recurrences in this set. It will have go
|
| 185 |
+
through the whole recurrence, if this hasn't been done before. """
|
| 186 |
+
if self._len is None:
|
| 187 |
+
for x in self:
|
| 188 |
+
pass
|
| 189 |
+
return self._len
|
| 190 |
+
|
| 191 |
+
def before(self, dt, inc=False):
|
| 192 |
+
""" Returns the last recurrence before the given datetime instance. The
|
| 193 |
+
inc keyword defines what happens if dt is an occurrence. With
|
| 194 |
+
inc=True, if dt itself is an occurrence, it will be returned. """
|
| 195 |
+
if self._cache_complete:
|
| 196 |
+
gen = self._cache
|
| 197 |
+
else:
|
| 198 |
+
gen = self
|
| 199 |
+
last = None
|
| 200 |
+
if inc:
|
| 201 |
+
for i in gen:
|
| 202 |
+
if i > dt:
|
| 203 |
+
break
|
| 204 |
+
last = i
|
| 205 |
+
else:
|
| 206 |
+
for i in gen:
|
| 207 |
+
if i >= dt:
|
| 208 |
+
break
|
| 209 |
+
last = i
|
| 210 |
+
return last
|
| 211 |
+
|
| 212 |
+
def after(self, dt, inc=False):
|
| 213 |
+
""" Returns the first recurrence after the given datetime instance. The
|
| 214 |
+
inc keyword defines what happens if dt is an occurrence. With
|
| 215 |
+
inc=True, if dt itself is an occurrence, it will be returned. """
|
| 216 |
+
if self._cache_complete:
|
| 217 |
+
gen = self._cache
|
| 218 |
+
else:
|
| 219 |
+
gen = self
|
| 220 |
+
if inc:
|
| 221 |
+
for i in gen:
|
| 222 |
+
if i >= dt:
|
| 223 |
+
return i
|
| 224 |
+
else:
|
| 225 |
+
for i in gen:
|
| 226 |
+
if i > dt:
|
| 227 |
+
return i
|
| 228 |
+
return None
|
| 229 |
+
|
| 230 |
+
def xafter(self, dt, count=None, inc=False):
|
| 231 |
+
"""
|
| 232 |
+
Generator which yields up to `count` recurrences after the given
|
| 233 |
+
datetime instance, equivalent to `after`.
|
| 234 |
+
|
| 235 |
+
:param dt:
|
| 236 |
+
The datetime at which to start generating recurrences.
|
| 237 |
+
|
| 238 |
+
:param count:
|
| 239 |
+
The maximum number of recurrences to generate. If `None` (default),
|
| 240 |
+
dates are generated until the recurrence rule is exhausted.
|
| 241 |
+
|
| 242 |
+
:param inc:
|
| 243 |
+
If `dt` is an instance of the rule and `inc` is `True`, it is
|
| 244 |
+
included in the output.
|
| 245 |
+
|
| 246 |
+
:yields: Yields a sequence of `datetime` objects.
|
| 247 |
+
"""
|
| 248 |
+
|
| 249 |
+
if self._cache_complete:
|
| 250 |
+
gen = self._cache
|
| 251 |
+
else:
|
| 252 |
+
gen = self
|
| 253 |
+
|
| 254 |
+
# Select the comparison function
|
| 255 |
+
if inc:
|
| 256 |
+
comp = lambda dc, dtc: dc >= dtc
|
| 257 |
+
else:
|
| 258 |
+
comp = lambda dc, dtc: dc > dtc
|
| 259 |
+
|
| 260 |
+
# Generate dates
|
| 261 |
+
n = 0
|
| 262 |
+
for d in gen:
|
| 263 |
+
if comp(d, dt):
|
| 264 |
+
if count is not None:
|
| 265 |
+
n += 1
|
| 266 |
+
if n > count:
|
| 267 |
+
break
|
| 268 |
+
|
| 269 |
+
yield d
|
| 270 |
+
|
| 271 |
+
def between(self, after, before, inc=False, count=1):
|
| 272 |
+
""" Returns all the occurrences of the rrule between after and before.
|
| 273 |
+
The inc keyword defines what happens if after and/or before are
|
| 274 |
+
themselves occurrences. With inc=True, they will be included in the
|
| 275 |
+
list, if they are found in the recurrence set. """
|
| 276 |
+
if self._cache_complete:
|
| 277 |
+
gen = self._cache
|
| 278 |
+
else:
|
| 279 |
+
gen = self
|
| 280 |
+
started = False
|
| 281 |
+
l = []
|
| 282 |
+
if inc:
|
| 283 |
+
for i in gen:
|
| 284 |
+
if i > before:
|
| 285 |
+
break
|
| 286 |
+
elif not started:
|
| 287 |
+
if i >= after:
|
| 288 |
+
started = True
|
| 289 |
+
l.append(i)
|
| 290 |
+
else:
|
| 291 |
+
l.append(i)
|
| 292 |
+
else:
|
| 293 |
+
for i in gen:
|
| 294 |
+
if i >= before:
|
| 295 |
+
break
|
| 296 |
+
elif not started:
|
| 297 |
+
if i > after:
|
| 298 |
+
started = True
|
| 299 |
+
l.append(i)
|
| 300 |
+
else:
|
| 301 |
+
l.append(i)
|
| 302 |
+
return l
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
class rrule(rrulebase):
|
| 306 |
+
"""
|
| 307 |
+
That's the base of the rrule operation. It accepts all the keywords
|
| 308 |
+
defined in the RFC as its constructor parameters (except byday,
|
| 309 |
+
which was renamed to byweekday) and more. The constructor prototype is::
|
| 310 |
+
|
| 311 |
+
rrule(freq)
|
| 312 |
+
|
| 313 |
+
Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
|
| 314 |
+
or SECONDLY.
|
| 315 |
+
|
| 316 |
+
.. note::
|
| 317 |
+
Per RFC section 3.3.10, recurrence instances falling on invalid dates
|
| 318 |
+
and times are ignored rather than coerced:
|
| 319 |
+
|
| 320 |
+
Recurrence rules may generate recurrence instances with an invalid
|
| 321 |
+
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
|
| 322 |
+
on a day where the local time is moved forward by an hour at 1:00
|
| 323 |
+
AM). Such recurrence instances MUST be ignored and MUST NOT be
|
| 324 |
+
counted as part of the recurrence set.
|
| 325 |
+
|
| 326 |
+
This can lead to possibly surprising behavior when, for example, the
|
| 327 |
+
start date occurs at the end of the month:
|
| 328 |
+
|
| 329 |
+
>>> from dateutil.rrule import rrule, MONTHLY
|
| 330 |
+
>>> from datetime import datetime
|
| 331 |
+
>>> start_date = datetime(2014, 12, 31)
|
| 332 |
+
>>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
|
| 333 |
+
... # doctest: +NORMALIZE_WHITESPACE
|
| 334 |
+
[datetime.datetime(2014, 12, 31, 0, 0),
|
| 335 |
+
datetime.datetime(2015, 1, 31, 0, 0),
|
| 336 |
+
datetime.datetime(2015, 3, 31, 0, 0),
|
| 337 |
+
datetime.datetime(2015, 5, 31, 0, 0)]
|
| 338 |
+
|
| 339 |
+
Additionally, it supports the following keyword arguments:
|
| 340 |
+
|
| 341 |
+
:param dtstart:
|
| 342 |
+
The recurrence start. Besides being the base for the recurrence,
|
| 343 |
+
missing parameters in the final recurrence instances will also be
|
| 344 |
+
extracted from this date. If not given, datetime.now() will be used
|
| 345 |
+
instead.
|
| 346 |
+
:param interval:
|
| 347 |
+
The interval between each freq iteration. For example, when using
|
| 348 |
+
YEARLY, an interval of 2 means once every two years, but with HOURLY,
|
| 349 |
+
it means once every two hours. The default interval is 1.
|
| 350 |
+
:param wkst:
|
| 351 |
+
The week start day. Must be one of the MO, TU, WE constants, or an
|
| 352 |
+
integer, specifying the first day of the week. This will affect
|
| 353 |
+
recurrences based on weekly periods. The default week start is got
|
| 354 |
+
from calendar.firstweekday(), and may be modified by
|
| 355 |
+
calendar.setfirstweekday().
|
| 356 |
+
:param count:
|
| 357 |
+
If given, this determines how many occurrences will be generated.
|
| 358 |
+
|
| 359 |
+
.. note::
|
| 360 |
+
As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
| 361 |
+
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
| 362 |
+
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
| 363 |
+
html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
| 364 |
+
**must not** occur in the same call to ``rrule``.
|
| 365 |
+
:param until:
|
| 366 |
+
If given, this must be a datetime instance specifying the upper-bound
|
| 367 |
+
limit of the recurrence. The last recurrence in the rule is the greatest
|
| 368 |
+
datetime that is less than or equal to the value specified in the
|
| 369 |
+
``until`` parameter.
|
| 370 |
+
|
| 371 |
+
.. note::
|
| 372 |
+
As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
| 373 |
+
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
| 374 |
+
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
| 375 |
+
html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
| 376 |
+
**must not** occur in the same call to ``rrule``.
|
| 377 |
+
:param bysetpos:
|
| 378 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 379 |
+
positive or negative. Each given integer will specify an occurrence
|
| 380 |
+
number, corresponding to the nth occurrence of the rule inside the
|
| 381 |
+
frequency period. For example, a bysetpos of -1 if combined with a
|
| 382 |
+
MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
|
| 383 |
+
result in the last work day of every month.
|
| 384 |
+
:param bymonth:
|
| 385 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 386 |
+
meaning the months to apply the recurrence to.
|
| 387 |
+
:param bymonthday:
|
| 388 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 389 |
+
meaning the month days to apply the recurrence to.
|
| 390 |
+
:param byyearday:
|
| 391 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 392 |
+
meaning the year days to apply the recurrence to.
|
| 393 |
+
:param byeaster:
|
| 394 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 395 |
+
positive or negative. Each integer will define an offset from the
|
| 396 |
+
Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
|
| 397 |
+
Sunday itself. This is an extension to the RFC specification.
|
| 398 |
+
:param byweekno:
|
| 399 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 400 |
+
meaning the week numbers to apply the recurrence to. Week numbers
|
| 401 |
+
have the meaning described in ISO8601, that is, the first week of
|
| 402 |
+
the year is that containing at least four days of the new year.
|
| 403 |
+
:param byweekday:
|
| 404 |
+
If given, it must be either an integer (0 == MO), a sequence of
|
| 405 |
+
integers, one of the weekday constants (MO, TU, etc), or a sequence
|
| 406 |
+
of these constants. When given, these variables will define the
|
| 407 |
+
weekdays where the recurrence will be applied. It's also possible to
|
| 408 |
+
use an argument n for the weekday instances, which will mean the nth
|
| 409 |
+
occurrence of this weekday in the period. For example, with MONTHLY,
|
| 410 |
+
or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
|
| 411 |
+
first friday of the month where the recurrence happens. Notice that in
|
| 412 |
+
the RFC documentation, this is specified as BYDAY, but was renamed to
|
| 413 |
+
avoid the ambiguity of that keyword.
|
| 414 |
+
:param byhour:
|
| 415 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 416 |
+
meaning the hours to apply the recurrence to.
|
| 417 |
+
:param byminute:
|
| 418 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 419 |
+
meaning the minutes to apply the recurrence to.
|
| 420 |
+
:param bysecond:
|
| 421 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 422 |
+
meaning the seconds to apply the recurrence to.
|
| 423 |
+
:param cache:
|
| 424 |
+
If given, it must be a boolean value specifying to enable or disable
|
| 425 |
+
caching of results. If you will use the same rrule instance multiple
|
| 426 |
+
times, enabling caching will improve the performance considerably.
|
| 427 |
+
"""
|
| 428 |
+
def __init__(self, freq, dtstart=None,
|
| 429 |
+
interval=1, wkst=None, count=None, until=None, bysetpos=None,
|
| 430 |
+
bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
|
| 431 |
+
byweekno=None, byweekday=None,
|
| 432 |
+
byhour=None, byminute=None, bysecond=None,
|
| 433 |
+
cache=False):
|
| 434 |
+
super(rrule, self).__init__(cache)
|
| 435 |
+
global easter
|
| 436 |
+
if not dtstart:
|
| 437 |
+
if until and until.tzinfo:
|
| 438 |
+
dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
|
| 439 |
+
else:
|
| 440 |
+
dtstart = datetime.datetime.now().replace(microsecond=0)
|
| 441 |
+
elif not isinstance(dtstart, datetime.datetime):
|
| 442 |
+
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
|
| 443 |
+
else:
|
| 444 |
+
dtstart = dtstart.replace(microsecond=0)
|
| 445 |
+
self._dtstart = dtstart
|
| 446 |
+
self._tzinfo = dtstart.tzinfo
|
| 447 |
+
self._freq = freq
|
| 448 |
+
self._interval = interval
|
| 449 |
+
self._count = count
|
| 450 |
+
|
| 451 |
+
# Cache the original byxxx rules, if they are provided, as the _byxxx
|
| 452 |
+
# attributes do not necessarily map to the inputs, and this can be
|
| 453 |
+
# a problem in generating the strings. Only store things if they've
|
| 454 |
+
# been supplied (the string retrieval will just use .get())
|
| 455 |
+
self._original_rule = {}
|
| 456 |
+
|
| 457 |
+
if until and not isinstance(until, datetime.datetime):
|
| 458 |
+
until = datetime.datetime.fromordinal(until.toordinal())
|
| 459 |
+
self._until = until
|
| 460 |
+
|
| 461 |
+
if self._dtstart and self._until:
|
| 462 |
+
if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
|
| 463 |
+
# According to RFC5545 Section 3.3.10:
|
| 464 |
+
# https://tools.ietf.org/html/rfc5545#section-3.3.10
|
| 465 |
+
#
|
| 466 |
+
# > If the "DTSTART" property is specified as a date with UTC
|
| 467 |
+
# > time or a date with local time and time zone reference,
|
| 468 |
+
# > then the UNTIL rule part MUST be specified as a date with
|
| 469 |
+
# > UTC time.
|
| 470 |
+
raise ValueError(
|
| 471 |
+
'RRULE UNTIL values must be specified in UTC when DTSTART '
|
| 472 |
+
'is timezone-aware'
|
| 473 |
+
)
|
| 474 |
+
|
| 475 |
+
if count is not None and until:
|
| 476 |
+
warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
| 477 |
+
" and has been deprecated in dateutil. Future versions will "
|
| 478 |
+
"raise an error.", DeprecationWarning)
|
| 479 |
+
|
| 480 |
+
if wkst is None:
|
| 481 |
+
self._wkst = calendar.firstweekday()
|
| 482 |
+
elif isinstance(wkst, integer_types):
|
| 483 |
+
self._wkst = wkst
|
| 484 |
+
else:
|
| 485 |
+
self._wkst = wkst.weekday
|
| 486 |
+
|
| 487 |
+
if bysetpos is None:
|
| 488 |
+
self._bysetpos = None
|
| 489 |
+
elif isinstance(bysetpos, integer_types):
|
| 490 |
+
if bysetpos == 0 or not (-366 <= bysetpos <= 366):
|
| 491 |
+
raise ValueError("bysetpos must be between 1 and 366, "
|
| 492 |
+
"or between -366 and -1")
|
| 493 |
+
self._bysetpos = (bysetpos,)
|
| 494 |
+
else:
|
| 495 |
+
self._bysetpos = tuple(bysetpos)
|
| 496 |
+
for pos in self._bysetpos:
|
| 497 |
+
if pos == 0 or not (-366 <= pos <= 366):
|
| 498 |
+
raise ValueError("bysetpos must be between 1 and 366, "
|
| 499 |
+
"or between -366 and -1")
|
| 500 |
+
|
| 501 |
+
if self._bysetpos:
|
| 502 |
+
self._original_rule['bysetpos'] = self._bysetpos
|
| 503 |
+
|
| 504 |
+
if (byweekno is None and byyearday is None and bymonthday is None and
|
| 505 |
+
byweekday is None and byeaster is None):
|
| 506 |
+
if freq == YEARLY:
|
| 507 |
+
if bymonth is None:
|
| 508 |
+
bymonth = dtstart.month
|
| 509 |
+
self._original_rule['bymonth'] = None
|
| 510 |
+
bymonthday = dtstart.day
|
| 511 |
+
self._original_rule['bymonthday'] = None
|
| 512 |
+
elif freq == MONTHLY:
|
| 513 |
+
bymonthday = dtstart.day
|
| 514 |
+
self._original_rule['bymonthday'] = None
|
| 515 |
+
elif freq == WEEKLY:
|
| 516 |
+
byweekday = dtstart.weekday()
|
| 517 |
+
self._original_rule['byweekday'] = None
|
| 518 |
+
|
| 519 |
+
# bymonth
|
| 520 |
+
if bymonth is None:
|
| 521 |
+
self._bymonth = None
|
| 522 |
+
else:
|
| 523 |
+
if isinstance(bymonth, integer_types):
|
| 524 |
+
bymonth = (bymonth,)
|
| 525 |
+
|
| 526 |
+
self._bymonth = tuple(sorted(set(bymonth)))
|
| 527 |
+
|
| 528 |
+
if 'bymonth' not in self._original_rule:
|
| 529 |
+
self._original_rule['bymonth'] = self._bymonth
|
| 530 |
+
|
| 531 |
+
# byyearday
|
| 532 |
+
if byyearday is None:
|
| 533 |
+
self._byyearday = None
|
| 534 |
+
else:
|
| 535 |
+
if isinstance(byyearday, integer_types):
|
| 536 |
+
byyearday = (byyearday,)
|
| 537 |
+
|
| 538 |
+
self._byyearday = tuple(sorted(set(byyearday)))
|
| 539 |
+
self._original_rule['byyearday'] = self._byyearday
|
| 540 |
+
|
| 541 |
+
# byeaster
|
| 542 |
+
if byeaster is not None:
|
| 543 |
+
if not easter:
|
| 544 |
+
from dateutil import easter
|
| 545 |
+
if isinstance(byeaster, integer_types):
|
| 546 |
+
self._byeaster = (byeaster,)
|
| 547 |
+
else:
|
| 548 |
+
self._byeaster = tuple(sorted(byeaster))
|
| 549 |
+
|
| 550 |
+
self._original_rule['byeaster'] = self._byeaster
|
| 551 |
+
else:
|
| 552 |
+
self._byeaster = None
|
| 553 |
+
|
| 554 |
+
# bymonthday
|
| 555 |
+
if bymonthday is None:
|
| 556 |
+
self._bymonthday = ()
|
| 557 |
+
self._bynmonthday = ()
|
| 558 |
+
else:
|
| 559 |
+
if isinstance(bymonthday, integer_types):
|
| 560 |
+
bymonthday = (bymonthday,)
|
| 561 |
+
|
| 562 |
+
bymonthday = set(bymonthday) # Ensure it's unique
|
| 563 |
+
|
| 564 |
+
self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
|
| 565 |
+
self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
|
| 566 |
+
|
| 567 |
+
# Storing positive numbers first, then negative numbers
|
| 568 |
+
if 'bymonthday' not in self._original_rule:
|
| 569 |
+
self._original_rule['bymonthday'] = tuple(
|
| 570 |
+
itertools.chain(self._bymonthday, self._bynmonthday))
|
| 571 |
+
|
| 572 |
+
# byweekno
|
| 573 |
+
if byweekno is None:
|
| 574 |
+
self._byweekno = None
|
| 575 |
+
else:
|
| 576 |
+
if isinstance(byweekno, integer_types):
|
| 577 |
+
byweekno = (byweekno,)
|
| 578 |
+
|
| 579 |
+
self._byweekno = tuple(sorted(set(byweekno)))
|
| 580 |
+
|
| 581 |
+
self._original_rule['byweekno'] = self._byweekno
|
| 582 |
+
|
| 583 |
+
# byweekday / bynweekday
|
| 584 |
+
if byweekday is None:
|
| 585 |
+
self._byweekday = None
|
| 586 |
+
self._bynweekday = None
|
| 587 |
+
else:
|
| 588 |
+
# If it's one of the valid non-sequence types, convert to a
|
| 589 |
+
# single-element sequence before the iterator that builds the
|
| 590 |
+
# byweekday set.
|
| 591 |
+
if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
|
| 592 |
+
byweekday = (byweekday,)
|
| 593 |
+
|
| 594 |
+
self._byweekday = set()
|
| 595 |
+
self._bynweekday = set()
|
| 596 |
+
for wday in byweekday:
|
| 597 |
+
if isinstance(wday, integer_types):
|
| 598 |
+
self._byweekday.add(wday)
|
| 599 |
+
elif not wday.n or freq > MONTHLY:
|
| 600 |
+
self._byweekday.add(wday.weekday)
|
| 601 |
+
else:
|
| 602 |
+
self._bynweekday.add((wday.weekday, wday.n))
|
| 603 |
+
|
| 604 |
+
if not self._byweekday:
|
| 605 |
+
self._byweekday = None
|
| 606 |
+
elif not self._bynweekday:
|
| 607 |
+
self._bynweekday = None
|
| 608 |
+
|
| 609 |
+
if self._byweekday is not None:
|
| 610 |
+
self._byweekday = tuple(sorted(self._byweekday))
|
| 611 |
+
orig_byweekday = [weekday(x) for x in self._byweekday]
|
| 612 |
+
else:
|
| 613 |
+
orig_byweekday = ()
|
| 614 |
+
|
| 615 |
+
if self._bynweekday is not None:
|
| 616 |
+
self._bynweekday = tuple(sorted(self._bynweekday))
|
| 617 |
+
orig_bynweekday = [weekday(*x) for x in self._bynweekday]
|
| 618 |
+
else:
|
| 619 |
+
orig_bynweekday = ()
|
| 620 |
+
|
| 621 |
+
if 'byweekday' not in self._original_rule:
|
| 622 |
+
self._original_rule['byweekday'] = tuple(itertools.chain(
|
| 623 |
+
orig_byweekday, orig_bynweekday))
|
| 624 |
+
|
| 625 |
+
# byhour
|
| 626 |
+
if byhour is None:
|
| 627 |
+
if freq < HOURLY:
|
| 628 |
+
self._byhour = {dtstart.hour}
|
| 629 |
+
else:
|
| 630 |
+
self._byhour = None
|
| 631 |
+
else:
|
| 632 |
+
if isinstance(byhour, integer_types):
|
| 633 |
+
byhour = (byhour,)
|
| 634 |
+
|
| 635 |
+
if freq == HOURLY:
|
| 636 |
+
self._byhour = self.__construct_byset(start=dtstart.hour,
|
| 637 |
+
byxxx=byhour,
|
| 638 |
+
base=24)
|
| 639 |
+
else:
|
| 640 |
+
self._byhour = set(byhour)
|
| 641 |
+
|
| 642 |
+
self._byhour = tuple(sorted(self._byhour))
|
| 643 |
+
self._original_rule['byhour'] = self._byhour
|
| 644 |
+
|
| 645 |
+
# byminute
|
| 646 |
+
if byminute is None:
|
| 647 |
+
if freq < MINUTELY:
|
| 648 |
+
self._byminute = {dtstart.minute}
|
| 649 |
+
else:
|
| 650 |
+
self._byminute = None
|
| 651 |
+
else:
|
| 652 |
+
if isinstance(byminute, integer_types):
|
| 653 |
+
byminute = (byminute,)
|
| 654 |
+
|
| 655 |
+
if freq == MINUTELY:
|
| 656 |
+
self._byminute = self.__construct_byset(start=dtstart.minute,
|
| 657 |
+
byxxx=byminute,
|
| 658 |
+
base=60)
|
| 659 |
+
else:
|
| 660 |
+
self._byminute = set(byminute)
|
| 661 |
+
|
| 662 |
+
self._byminute = tuple(sorted(self._byminute))
|
| 663 |
+
self._original_rule['byminute'] = self._byminute
|
| 664 |
+
|
| 665 |
+
# bysecond
|
| 666 |
+
if bysecond is None:
|
| 667 |
+
if freq < SECONDLY:
|
| 668 |
+
self._bysecond = ((dtstart.second,))
|
| 669 |
+
else:
|
| 670 |
+
self._bysecond = None
|
| 671 |
+
else:
|
| 672 |
+
if isinstance(bysecond, integer_types):
|
| 673 |
+
bysecond = (bysecond,)
|
| 674 |
+
|
| 675 |
+
self._bysecond = set(bysecond)
|
| 676 |
+
|
| 677 |
+
if freq == SECONDLY:
|
| 678 |
+
self._bysecond = self.__construct_byset(start=dtstart.second,
|
| 679 |
+
byxxx=bysecond,
|
| 680 |
+
base=60)
|
| 681 |
+
else:
|
| 682 |
+
self._bysecond = set(bysecond)
|
| 683 |
+
|
| 684 |
+
self._bysecond = tuple(sorted(self._bysecond))
|
| 685 |
+
self._original_rule['bysecond'] = self._bysecond
|
| 686 |
+
|
| 687 |
+
if self._freq >= HOURLY:
|
| 688 |
+
self._timeset = None
|
| 689 |
+
else:
|
| 690 |
+
self._timeset = []
|
| 691 |
+
for hour in self._byhour:
|
| 692 |
+
for minute in self._byminute:
|
| 693 |
+
for second in self._bysecond:
|
| 694 |
+
self._timeset.append(
|
| 695 |
+
datetime.time(hour, minute, second,
|
| 696 |
+
tzinfo=self._tzinfo))
|
| 697 |
+
self._timeset.sort()
|
| 698 |
+
self._timeset = tuple(self._timeset)
|
| 699 |
+
|
| 700 |
+
def __str__(self):
|
| 701 |
+
"""
|
| 702 |
+
Output a string that would generate this RRULE if passed to rrulestr.
|
| 703 |
+
This is mostly compatible with RFC5545, except for the
|
| 704 |
+
dateutil-specific extension BYEASTER.
|
| 705 |
+
"""
|
| 706 |
+
|
| 707 |
+
output = []
|
| 708 |
+
h, m, s = [None] * 3
|
| 709 |
+
if self._dtstart:
|
| 710 |
+
output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
|
| 711 |
+
h, m, s = self._dtstart.timetuple()[3:6]
|
| 712 |
+
|
| 713 |
+
parts = ['FREQ=' + FREQNAMES[self._freq]]
|
| 714 |
+
if self._interval != 1:
|
| 715 |
+
parts.append('INTERVAL=' + str(self._interval))
|
| 716 |
+
|
| 717 |
+
if self._wkst:
|
| 718 |
+
parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
|
| 719 |
+
|
| 720 |
+
if self._count is not None:
|
| 721 |
+
parts.append('COUNT=' + str(self._count))
|
| 722 |
+
|
| 723 |
+
if self._until:
|
| 724 |
+
parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
|
| 725 |
+
|
| 726 |
+
if self._original_rule.get('byweekday') is not None:
|
| 727 |
+
# The str() method on weekday objects doesn't generate
|
| 728 |
+
# RFC5545-compliant strings, so we should modify that.
|
| 729 |
+
original_rule = dict(self._original_rule)
|
| 730 |
+
wday_strings = []
|
| 731 |
+
for wday in original_rule['byweekday']:
|
| 732 |
+
if wday.n:
|
| 733 |
+
wday_strings.append('{n:+d}{wday}'.format(
|
| 734 |
+
n=wday.n,
|
| 735 |
+
wday=repr(wday)[0:2]))
|
| 736 |
+
else:
|
| 737 |
+
wday_strings.append(repr(wday))
|
| 738 |
+
|
| 739 |
+
original_rule['byweekday'] = wday_strings
|
| 740 |
+
else:
|
| 741 |
+
original_rule = self._original_rule
|
| 742 |
+
|
| 743 |
+
partfmt = '{name}={vals}'
|
| 744 |
+
for name, key in [('BYSETPOS', 'bysetpos'),
|
| 745 |
+
('BYMONTH', 'bymonth'),
|
| 746 |
+
('BYMONTHDAY', 'bymonthday'),
|
| 747 |
+
('BYYEARDAY', 'byyearday'),
|
| 748 |
+
('BYWEEKNO', 'byweekno'),
|
| 749 |
+
('BYDAY', 'byweekday'),
|
| 750 |
+
('BYHOUR', 'byhour'),
|
| 751 |
+
('BYMINUTE', 'byminute'),
|
| 752 |
+
('BYSECOND', 'bysecond'),
|
| 753 |
+
('BYEASTER', 'byeaster')]:
|
| 754 |
+
value = original_rule.get(key)
|
| 755 |
+
if value:
|
| 756 |
+
parts.append(partfmt.format(name=name, vals=(','.join(str(v)
|
| 757 |
+
for v in value))))
|
| 758 |
+
|
| 759 |
+
output.append('RRULE:' + ';'.join(parts))
|
| 760 |
+
return '\n'.join(output)
|
| 761 |
+
|
| 762 |
+
def replace(self, **kwargs):
|
| 763 |
+
"""Return new rrule with same attributes except for those attributes given new
|
| 764 |
+
values by whichever keyword arguments are specified."""
|
| 765 |
+
new_kwargs = {"interval": self._interval,
|
| 766 |
+
"count": self._count,
|
| 767 |
+
"dtstart": self._dtstart,
|
| 768 |
+
"freq": self._freq,
|
| 769 |
+
"until": self._until,
|
| 770 |
+
"wkst": self._wkst,
|
| 771 |
+
"cache": False if self._cache is None else True }
|
| 772 |
+
new_kwargs.update(self._original_rule)
|
| 773 |
+
new_kwargs.update(kwargs)
|
| 774 |
+
return rrule(**new_kwargs)
|
| 775 |
+
|
| 776 |
+
def _iter(self):
|
| 777 |
+
year, month, day, hour, minute, second, weekday, yearday, _ = \
|
| 778 |
+
self._dtstart.timetuple()
|
| 779 |
+
|
| 780 |
+
# Some local variables to speed things up a bit
|
| 781 |
+
freq = self._freq
|
| 782 |
+
interval = self._interval
|
| 783 |
+
wkst = self._wkst
|
| 784 |
+
until = self._until
|
| 785 |
+
bymonth = self._bymonth
|
| 786 |
+
byweekno = self._byweekno
|
| 787 |
+
byyearday = self._byyearday
|
| 788 |
+
byweekday = self._byweekday
|
| 789 |
+
byeaster = self._byeaster
|
| 790 |
+
bymonthday = self._bymonthday
|
| 791 |
+
bynmonthday = self._bynmonthday
|
| 792 |
+
bysetpos = self._bysetpos
|
| 793 |
+
byhour = self._byhour
|
| 794 |
+
byminute = self._byminute
|
| 795 |
+
bysecond = self._bysecond
|
| 796 |
+
|
| 797 |
+
ii = _iterinfo(self)
|
| 798 |
+
ii.rebuild(year, month)
|
| 799 |
+
|
| 800 |
+
getdayset = {YEARLY: ii.ydayset,
|
| 801 |
+
MONTHLY: ii.mdayset,
|
| 802 |
+
WEEKLY: ii.wdayset,
|
| 803 |
+
DAILY: ii.ddayset,
|
| 804 |
+
HOURLY: ii.ddayset,
|
| 805 |
+
MINUTELY: ii.ddayset,
|
| 806 |
+
SECONDLY: ii.ddayset}[freq]
|
| 807 |
+
|
| 808 |
+
if freq < HOURLY:
|
| 809 |
+
timeset = self._timeset
|
| 810 |
+
else:
|
| 811 |
+
gettimeset = {HOURLY: ii.htimeset,
|
| 812 |
+
MINUTELY: ii.mtimeset,
|
| 813 |
+
SECONDLY: ii.stimeset}[freq]
|
| 814 |
+
if ((freq >= HOURLY and
|
| 815 |
+
self._byhour and hour not in self._byhour) or
|
| 816 |
+
(freq >= MINUTELY and
|
| 817 |
+
self._byminute and minute not in self._byminute) or
|
| 818 |
+
(freq >= SECONDLY and
|
| 819 |
+
self._bysecond and second not in self._bysecond)):
|
| 820 |
+
timeset = ()
|
| 821 |
+
else:
|
| 822 |
+
timeset = gettimeset(hour, minute, second)
|
| 823 |
+
|
| 824 |
+
total = 0
|
| 825 |
+
count = self._count
|
| 826 |
+
while True:
|
| 827 |
+
# Get dayset with the right frequency
|
| 828 |
+
dayset, start, end = getdayset(year, month, day)
|
| 829 |
+
|
| 830 |
+
# Do the "hard" work ;-)
|
| 831 |
+
filtered = False
|
| 832 |
+
for i in dayset[start:end]:
|
| 833 |
+
if ((bymonth and ii.mmask[i] not in bymonth) or
|
| 834 |
+
(byweekno and not ii.wnomask[i]) or
|
| 835 |
+
(byweekday and ii.wdaymask[i] not in byweekday) or
|
| 836 |
+
(ii.nwdaymask and not ii.nwdaymask[i]) or
|
| 837 |
+
(byeaster and not ii.eastermask[i]) or
|
| 838 |
+
((bymonthday or bynmonthday) and
|
| 839 |
+
ii.mdaymask[i] not in bymonthday and
|
| 840 |
+
ii.nmdaymask[i] not in bynmonthday) or
|
| 841 |
+
(byyearday and
|
| 842 |
+
((i < ii.yearlen and i+1 not in byyearday and
|
| 843 |
+
-ii.yearlen+i not in byyearday) or
|
| 844 |
+
(i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
|
| 845 |
+
-ii.nextyearlen+i-ii.yearlen not in byyearday)))):
|
| 846 |
+
dayset[i] = None
|
| 847 |
+
filtered = True
|
| 848 |
+
|
| 849 |
+
# Output results
|
| 850 |
+
if bysetpos and timeset:
|
| 851 |
+
poslist = []
|
| 852 |
+
for pos in bysetpos:
|
| 853 |
+
if pos < 0:
|
| 854 |
+
daypos, timepos = divmod(pos, len(timeset))
|
| 855 |
+
else:
|
| 856 |
+
daypos, timepos = divmod(pos-1, len(timeset))
|
| 857 |
+
try:
|
| 858 |
+
i = [x for x in dayset[start:end]
|
| 859 |
+
if x is not None][daypos]
|
| 860 |
+
time = timeset[timepos]
|
| 861 |
+
except IndexError:
|
| 862 |
+
pass
|
| 863 |
+
else:
|
| 864 |
+
date = datetime.date.fromordinal(ii.yearordinal+i)
|
| 865 |
+
res = datetime.datetime.combine(date, time)
|
| 866 |
+
if res not in poslist:
|
| 867 |
+
poslist.append(res)
|
| 868 |
+
poslist.sort()
|
| 869 |
+
for res in poslist:
|
| 870 |
+
if until and res > until:
|
| 871 |
+
self._len = total
|
| 872 |
+
return
|
| 873 |
+
elif res >= self._dtstart:
|
| 874 |
+
if count is not None:
|
| 875 |
+
count -= 1
|
| 876 |
+
if count < 0:
|
| 877 |
+
self._len = total
|
| 878 |
+
return
|
| 879 |
+
total += 1
|
| 880 |
+
yield res
|
| 881 |
+
else:
|
| 882 |
+
for i in dayset[start:end]:
|
| 883 |
+
if i is not None:
|
| 884 |
+
date = datetime.date.fromordinal(ii.yearordinal + i)
|
| 885 |
+
for time in timeset:
|
| 886 |
+
res = datetime.datetime.combine(date, time)
|
| 887 |
+
if until and res > until:
|
| 888 |
+
self._len = total
|
| 889 |
+
return
|
| 890 |
+
elif res >= self._dtstart:
|
| 891 |
+
if count is not None:
|
| 892 |
+
count -= 1
|
| 893 |
+
if count < 0:
|
| 894 |
+
self._len = total
|
| 895 |
+
return
|
| 896 |
+
|
| 897 |
+
total += 1
|
| 898 |
+
yield res
|
| 899 |
+
|
| 900 |
+
# Handle frequency and interval
|
| 901 |
+
fixday = False
|
| 902 |
+
if freq == YEARLY:
|
| 903 |
+
year += interval
|
| 904 |
+
if year > datetime.MAXYEAR:
|
| 905 |
+
self._len = total
|
| 906 |
+
return
|
| 907 |
+
ii.rebuild(year, month)
|
| 908 |
+
elif freq == MONTHLY:
|
| 909 |
+
month += interval
|
| 910 |
+
if month > 12:
|
| 911 |
+
div, mod = divmod(month, 12)
|
| 912 |
+
month = mod
|
| 913 |
+
year += div
|
| 914 |
+
if month == 0:
|
| 915 |
+
month = 12
|
| 916 |
+
year -= 1
|
| 917 |
+
if year > datetime.MAXYEAR:
|
| 918 |
+
self._len = total
|
| 919 |
+
return
|
| 920 |
+
ii.rebuild(year, month)
|
| 921 |
+
elif freq == WEEKLY:
|
| 922 |
+
if wkst > weekday:
|
| 923 |
+
day += -(weekday+1+(6-wkst))+self._interval*7
|
| 924 |
+
else:
|
| 925 |
+
day += -(weekday-wkst)+self._interval*7
|
| 926 |
+
weekday = wkst
|
| 927 |
+
fixday = True
|
| 928 |
+
elif freq == DAILY:
|
| 929 |
+
day += interval
|
| 930 |
+
fixday = True
|
| 931 |
+
elif freq == HOURLY:
|
| 932 |
+
if filtered:
|
| 933 |
+
# Jump to one iteration before next day
|
| 934 |
+
hour += ((23-hour)//interval)*interval
|
| 935 |
+
|
| 936 |
+
if byhour:
|
| 937 |
+
ndays, hour = self.__mod_distance(value=hour,
|
| 938 |
+
byxxx=self._byhour,
|
| 939 |
+
base=24)
|
| 940 |
+
else:
|
| 941 |
+
ndays, hour = divmod(hour+interval, 24)
|
| 942 |
+
|
| 943 |
+
if ndays:
|
| 944 |
+
day += ndays
|
| 945 |
+
fixday = True
|
| 946 |
+
|
| 947 |
+
timeset = gettimeset(hour, minute, second)
|
| 948 |
+
elif freq == MINUTELY:
|
| 949 |
+
if filtered:
|
| 950 |
+
# Jump to one iteration before next day
|
| 951 |
+
minute += ((1439-(hour*60+minute))//interval)*interval
|
| 952 |
+
|
| 953 |
+
valid = False
|
| 954 |
+
rep_rate = (24*60)
|
| 955 |
+
for j in range(rep_rate // gcd(interval, rep_rate)):
|
| 956 |
+
if byminute:
|
| 957 |
+
nhours, minute = \
|
| 958 |
+
self.__mod_distance(value=minute,
|
| 959 |
+
byxxx=self._byminute,
|
| 960 |
+
base=60)
|
| 961 |
+
else:
|
| 962 |
+
nhours, minute = divmod(minute+interval, 60)
|
| 963 |
+
|
| 964 |
+
div, hour = divmod(hour+nhours, 24)
|
| 965 |
+
if div:
|
| 966 |
+
day += div
|
| 967 |
+
fixday = True
|
| 968 |
+
filtered = False
|
| 969 |
+
|
| 970 |
+
if not byhour or hour in byhour:
|
| 971 |
+
valid = True
|
| 972 |
+
break
|
| 973 |
+
|
| 974 |
+
if not valid:
|
| 975 |
+
raise ValueError('Invalid combination of interval and ' +
|
| 976 |
+
'byhour resulting in empty rule.')
|
| 977 |
+
|
| 978 |
+
timeset = gettimeset(hour, minute, second)
|
| 979 |
+
elif freq == SECONDLY:
|
| 980 |
+
if filtered:
|
| 981 |
+
# Jump to one iteration before next day
|
| 982 |
+
second += (((86399 - (hour * 3600 + minute * 60 + second))
|
| 983 |
+
// interval) * interval)
|
| 984 |
+
|
| 985 |
+
rep_rate = (24 * 3600)
|
| 986 |
+
valid = False
|
| 987 |
+
for j in range(0, rep_rate // gcd(interval, rep_rate)):
|
| 988 |
+
if bysecond:
|
| 989 |
+
nminutes, second = \
|
| 990 |
+
self.__mod_distance(value=second,
|
| 991 |
+
byxxx=self._bysecond,
|
| 992 |
+
base=60)
|
| 993 |
+
else:
|
| 994 |
+
nminutes, second = divmod(second+interval, 60)
|
| 995 |
+
|
| 996 |
+
div, minute = divmod(minute+nminutes, 60)
|
| 997 |
+
if div:
|
| 998 |
+
hour += div
|
| 999 |
+
div, hour = divmod(hour, 24)
|
| 1000 |
+
if div:
|
| 1001 |
+
day += div
|
| 1002 |
+
fixday = True
|
| 1003 |
+
|
| 1004 |
+
if ((not byhour or hour in byhour) and
|
| 1005 |
+
(not byminute or minute in byminute) and
|
| 1006 |
+
(not bysecond or second in bysecond)):
|
| 1007 |
+
valid = True
|
| 1008 |
+
break
|
| 1009 |
+
|
| 1010 |
+
if not valid:
|
| 1011 |
+
raise ValueError('Invalid combination of interval, ' +
|
| 1012 |
+
'byhour and byminute resulting in empty' +
|
| 1013 |
+
' rule.')
|
| 1014 |
+
|
| 1015 |
+
timeset = gettimeset(hour, minute, second)
|
| 1016 |
+
|
| 1017 |
+
if fixday and day > 28:
|
| 1018 |
+
daysinmonth = calendar.monthrange(year, month)[1]
|
| 1019 |
+
if day > daysinmonth:
|
| 1020 |
+
while day > daysinmonth:
|
| 1021 |
+
day -= daysinmonth
|
| 1022 |
+
month += 1
|
| 1023 |
+
if month == 13:
|
| 1024 |
+
month = 1
|
| 1025 |
+
year += 1
|
| 1026 |
+
if year > datetime.MAXYEAR:
|
| 1027 |
+
self._len = total
|
| 1028 |
+
return
|
| 1029 |
+
daysinmonth = calendar.monthrange(year, month)[1]
|
| 1030 |
+
ii.rebuild(year, month)
|
| 1031 |
+
|
| 1032 |
+
def __construct_byset(self, start, byxxx, base):
|
| 1033 |
+
"""
|
| 1034 |
+
If a `BYXXX` sequence is passed to the constructor at the same level as
|
| 1035 |
+
`FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
|
| 1036 |
+
specifications which cannot be reached given some starting conditions.
|
| 1037 |
+
|
| 1038 |
+
This occurs whenever the interval is not coprime with the base of a
|
| 1039 |
+
given unit and the difference between the starting position and the
|
| 1040 |
+
ending position is not coprime with the greatest common denominator
|
| 1041 |
+
between the interval and the base. For example, with a FREQ of hourly
|
| 1042 |
+
starting at 17:00 and an interval of 4, the only valid values for
|
| 1043 |
+
BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
|
| 1044 |
+
coprime.
|
| 1045 |
+
|
| 1046 |
+
:param start:
|
| 1047 |
+
Specifies the starting position.
|
| 1048 |
+
:param byxxx:
|
| 1049 |
+
An iterable containing the list of allowed values.
|
| 1050 |
+
:param base:
|
| 1051 |
+
The largest allowable value for the specified frequency (e.g.
|
| 1052 |
+
24 hours, 60 minutes).
|
| 1053 |
+
|
| 1054 |
+
This does not preserve the type of the iterable, returning a set, since
|
| 1055 |
+
the values should be unique and the order is irrelevant, this will
|
| 1056 |
+
speed up later lookups.
|
| 1057 |
+
|
| 1058 |
+
In the event of an empty set, raises a :exception:`ValueError`, as this
|
| 1059 |
+
results in an empty rrule.
|
| 1060 |
+
"""
|
| 1061 |
+
|
| 1062 |
+
cset = set()
|
| 1063 |
+
|
| 1064 |
+
# Support a single byxxx value.
|
| 1065 |
+
if isinstance(byxxx, integer_types):
|
| 1066 |
+
byxxx = (byxxx, )
|
| 1067 |
+
|
| 1068 |
+
for num in byxxx:
|
| 1069 |
+
i_gcd = gcd(self._interval, base)
|
| 1070 |
+
# Use divmod rather than % because we need to wrap negative nums.
|
| 1071 |
+
if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
|
| 1072 |
+
cset.add(num)
|
| 1073 |
+
|
| 1074 |
+
if len(cset) == 0:
|
| 1075 |
+
raise ValueError("Invalid rrule byxxx generates an empty set.")
|
| 1076 |
+
|
| 1077 |
+
return cset
|
| 1078 |
+
|
| 1079 |
+
def __mod_distance(self, value, byxxx, base):
|
| 1080 |
+
"""
|
| 1081 |
+
Calculates the next value in a sequence where the `FREQ` parameter is
|
| 1082 |
+
specified along with a `BYXXX` parameter at the same "level"
|
| 1083 |
+
(e.g. `HOURLY` specified with `BYHOUR`).
|
| 1084 |
+
|
| 1085 |
+
:param value:
|
| 1086 |
+
The old value of the component.
|
| 1087 |
+
:param byxxx:
|
| 1088 |
+
The `BYXXX` set, which should have been generated by
|
| 1089 |
+
`rrule._construct_byset`, or something else which checks that a
|
| 1090 |
+
valid rule is present.
|
| 1091 |
+
:param base:
|
| 1092 |
+
The largest allowable value for the specified frequency (e.g.
|
| 1093 |
+
24 hours, 60 minutes).
|
| 1094 |
+
|
| 1095 |
+
If a valid value is not found after `base` iterations (the maximum
|
| 1096 |
+
number before the sequence would start to repeat), this raises a
|
| 1097 |
+
:exception:`ValueError`, as no valid values were found.
|
| 1098 |
+
|
| 1099 |
+
This returns a tuple of `divmod(n*interval, base)`, where `n` is the
|
| 1100 |
+
smallest number of `interval` repetitions until the next specified
|
| 1101 |
+
value in `byxxx` is found.
|
| 1102 |
+
"""
|
| 1103 |
+
accumulator = 0
|
| 1104 |
+
for ii in range(1, base + 1):
|
| 1105 |
+
# Using divmod() over % to account for negative intervals
|
| 1106 |
+
div, value = divmod(value + self._interval, base)
|
| 1107 |
+
accumulator += div
|
| 1108 |
+
if value in byxxx:
|
| 1109 |
+
return (accumulator, value)
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
class _iterinfo(object):
|
| 1113 |
+
__slots__ = ["rrule", "lastyear", "lastmonth",
|
| 1114 |
+
"yearlen", "nextyearlen", "yearordinal", "yearweekday",
|
| 1115 |
+
"mmask", "mrange", "mdaymask", "nmdaymask",
|
| 1116 |
+
"wdaymask", "wnomask", "nwdaymask", "eastermask"]
|
| 1117 |
+
|
| 1118 |
+
def __init__(self, rrule):
|
| 1119 |
+
for attr in self.__slots__:
|
| 1120 |
+
setattr(self, attr, None)
|
| 1121 |
+
self.rrule = rrule
|
| 1122 |
+
|
| 1123 |
+
def rebuild(self, year, month):
|
| 1124 |
+
# Every mask is 7 days longer to handle cross-year weekly periods.
|
| 1125 |
+
rr = self.rrule
|
| 1126 |
+
if year != self.lastyear:
|
| 1127 |
+
self.yearlen = 365 + calendar.isleap(year)
|
| 1128 |
+
self.nextyearlen = 365 + calendar.isleap(year + 1)
|
| 1129 |
+
firstyday = datetime.date(year, 1, 1)
|
| 1130 |
+
self.yearordinal = firstyday.toordinal()
|
| 1131 |
+
self.yearweekday = firstyday.weekday()
|
| 1132 |
+
|
| 1133 |
+
wday = datetime.date(year, 1, 1).weekday()
|
| 1134 |
+
if self.yearlen == 365:
|
| 1135 |
+
self.mmask = M365MASK
|
| 1136 |
+
self.mdaymask = MDAY365MASK
|
| 1137 |
+
self.nmdaymask = NMDAY365MASK
|
| 1138 |
+
self.wdaymask = WDAYMASK[wday:]
|
| 1139 |
+
self.mrange = M365RANGE
|
| 1140 |
+
else:
|
| 1141 |
+
self.mmask = M366MASK
|
| 1142 |
+
self.mdaymask = MDAY366MASK
|
| 1143 |
+
self.nmdaymask = NMDAY366MASK
|
| 1144 |
+
self.wdaymask = WDAYMASK[wday:]
|
| 1145 |
+
self.mrange = M366RANGE
|
| 1146 |
+
|
| 1147 |
+
if not rr._byweekno:
|
| 1148 |
+
self.wnomask = None
|
| 1149 |
+
else:
|
| 1150 |
+
self.wnomask = [0]*(self.yearlen+7)
|
| 1151 |
+
# no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
|
| 1152 |
+
no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
|
| 1153 |
+
if no1wkst >= 4:
|
| 1154 |
+
no1wkst = 0
|
| 1155 |
+
# Number of days in the year, plus the days we got
|
| 1156 |
+
# from last year.
|
| 1157 |
+
wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
|
| 1158 |
+
else:
|
| 1159 |
+
# Number of days in the year, minus the days we
|
| 1160 |
+
# left in last year.
|
| 1161 |
+
wyearlen = self.yearlen-no1wkst
|
| 1162 |
+
div, mod = divmod(wyearlen, 7)
|
| 1163 |
+
numweeks = div+mod//4
|
| 1164 |
+
for n in rr._byweekno:
|
| 1165 |
+
if n < 0:
|
| 1166 |
+
n += numweeks+1
|
| 1167 |
+
if not (0 < n <= numweeks):
|
| 1168 |
+
continue
|
| 1169 |
+
if n > 1:
|
| 1170 |
+
i = no1wkst+(n-1)*7
|
| 1171 |
+
if no1wkst != firstwkst:
|
| 1172 |
+
i -= 7-firstwkst
|
| 1173 |
+
else:
|
| 1174 |
+
i = no1wkst
|
| 1175 |
+
for j in range(7):
|
| 1176 |
+
self.wnomask[i] = 1
|
| 1177 |
+
i += 1
|
| 1178 |
+
if self.wdaymask[i] == rr._wkst:
|
| 1179 |
+
break
|
| 1180 |
+
if 1 in rr._byweekno:
|
| 1181 |
+
# Check week number 1 of next year as well
|
| 1182 |
+
# TODO: Check -numweeks for next year.
|
| 1183 |
+
i = no1wkst+numweeks*7
|
| 1184 |
+
if no1wkst != firstwkst:
|
| 1185 |
+
i -= 7-firstwkst
|
| 1186 |
+
if i < self.yearlen:
|
| 1187 |
+
# If week starts in next year, we
|
| 1188 |
+
# don't care about it.
|
| 1189 |
+
for j in range(7):
|
| 1190 |
+
self.wnomask[i] = 1
|
| 1191 |
+
i += 1
|
| 1192 |
+
if self.wdaymask[i] == rr._wkst:
|
| 1193 |
+
break
|
| 1194 |
+
if no1wkst:
|
| 1195 |
+
# Check last week number of last year as
|
| 1196 |
+
# well. If no1wkst is 0, either the year
|
| 1197 |
+
# started on week start, or week number 1
|
| 1198 |
+
# got days from last year, so there are no
|
| 1199 |
+
# days from last year's last week number in
|
| 1200 |
+
# this year.
|
| 1201 |
+
if -1 not in rr._byweekno:
|
| 1202 |
+
lyearweekday = datetime.date(year-1, 1, 1).weekday()
|
| 1203 |
+
lno1wkst = (7-lyearweekday+rr._wkst) % 7
|
| 1204 |
+
lyearlen = 365+calendar.isleap(year-1)
|
| 1205 |
+
if lno1wkst >= 4:
|
| 1206 |
+
lno1wkst = 0
|
| 1207 |
+
lnumweeks = 52+(lyearlen +
|
| 1208 |
+
(lyearweekday-rr._wkst) % 7) % 7//4
|
| 1209 |
+
else:
|
| 1210 |
+
lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
|
| 1211 |
+
else:
|
| 1212 |
+
lnumweeks = -1
|
| 1213 |
+
if lnumweeks in rr._byweekno:
|
| 1214 |
+
for i in range(no1wkst):
|
| 1215 |
+
self.wnomask[i] = 1
|
| 1216 |
+
|
| 1217 |
+
if (rr._bynweekday and (month != self.lastmonth or
|
| 1218 |
+
year != self.lastyear)):
|
| 1219 |
+
ranges = []
|
| 1220 |
+
if rr._freq == YEARLY:
|
| 1221 |
+
if rr._bymonth:
|
| 1222 |
+
for month in rr._bymonth:
|
| 1223 |
+
ranges.append(self.mrange[month-1:month+1])
|
| 1224 |
+
else:
|
| 1225 |
+
ranges = [(0, self.yearlen)]
|
| 1226 |
+
elif rr._freq == MONTHLY:
|
| 1227 |
+
ranges = [self.mrange[month-1:month+1]]
|
| 1228 |
+
if ranges:
|
| 1229 |
+
# Weekly frequency won't get here, so we may not
|
| 1230 |
+
# care about cross-year weekly periods.
|
| 1231 |
+
self.nwdaymask = [0]*self.yearlen
|
| 1232 |
+
for first, last in ranges:
|
| 1233 |
+
last -= 1
|
| 1234 |
+
for wday, n in rr._bynweekday:
|
| 1235 |
+
if n < 0:
|
| 1236 |
+
i = last+(n+1)*7
|
| 1237 |
+
i -= (self.wdaymask[i]-wday) % 7
|
| 1238 |
+
else:
|
| 1239 |
+
i = first+(n-1)*7
|
| 1240 |
+
i += (7-self.wdaymask[i]+wday) % 7
|
| 1241 |
+
if first <= i <= last:
|
| 1242 |
+
self.nwdaymask[i] = 1
|
| 1243 |
+
|
| 1244 |
+
if rr._byeaster:
|
| 1245 |
+
self.eastermask = [0]*(self.yearlen+7)
|
| 1246 |
+
eyday = easter.easter(year).toordinal()-self.yearordinal
|
| 1247 |
+
for offset in rr._byeaster:
|
| 1248 |
+
self.eastermask[eyday+offset] = 1
|
| 1249 |
+
|
| 1250 |
+
self.lastyear = year
|
| 1251 |
+
self.lastmonth = month
|
| 1252 |
+
|
| 1253 |
+
def ydayset(self, year, month, day):
|
| 1254 |
+
return list(range(self.yearlen)), 0, self.yearlen
|
| 1255 |
+
|
| 1256 |
+
def mdayset(self, year, month, day):
|
| 1257 |
+
dset = [None]*self.yearlen
|
| 1258 |
+
start, end = self.mrange[month-1:month+1]
|
| 1259 |
+
for i in range(start, end):
|
| 1260 |
+
dset[i] = i
|
| 1261 |
+
return dset, start, end
|
| 1262 |
+
|
| 1263 |
+
def wdayset(self, year, month, day):
|
| 1264 |
+
# We need to handle cross-year weeks here.
|
| 1265 |
+
dset = [None]*(self.yearlen+7)
|
| 1266 |
+
i = datetime.date(year, month, day).toordinal()-self.yearordinal
|
| 1267 |
+
start = i
|
| 1268 |
+
for j in range(7):
|
| 1269 |
+
dset[i] = i
|
| 1270 |
+
i += 1
|
| 1271 |
+
# if (not (0 <= i < self.yearlen) or
|
| 1272 |
+
# self.wdaymask[i] == self.rrule._wkst):
|
| 1273 |
+
# This will cross the year boundary, if necessary.
|
| 1274 |
+
if self.wdaymask[i] == self.rrule._wkst:
|
| 1275 |
+
break
|
| 1276 |
+
return dset, start, i
|
| 1277 |
+
|
| 1278 |
+
def ddayset(self, year, month, day):
|
| 1279 |
+
dset = [None] * self.yearlen
|
| 1280 |
+
i = datetime.date(year, month, day).toordinal() - self.yearordinal
|
| 1281 |
+
dset[i] = i
|
| 1282 |
+
return dset, i, i + 1
|
| 1283 |
+
|
| 1284 |
+
def htimeset(self, hour, minute, second):
|
| 1285 |
+
tset = []
|
| 1286 |
+
rr = self.rrule
|
| 1287 |
+
for minute in rr._byminute:
|
| 1288 |
+
for second in rr._bysecond:
|
| 1289 |
+
tset.append(datetime.time(hour, minute, second,
|
| 1290 |
+
tzinfo=rr._tzinfo))
|
| 1291 |
+
tset.sort()
|
| 1292 |
+
return tset
|
| 1293 |
+
|
| 1294 |
+
def mtimeset(self, hour, minute, second):
|
| 1295 |
+
tset = []
|
| 1296 |
+
rr = self.rrule
|
| 1297 |
+
for second in rr._bysecond:
|
| 1298 |
+
tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
|
| 1299 |
+
tset.sort()
|
| 1300 |
+
return tset
|
| 1301 |
+
|
| 1302 |
+
def stimeset(self, hour, minute, second):
|
| 1303 |
+
return (datetime.time(hour, minute, second,
|
| 1304 |
+
tzinfo=self.rrule._tzinfo),)
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
class rruleset(rrulebase):
|
| 1308 |
+
""" The rruleset type allows more complex recurrence setups, mixing
|
| 1309 |
+
multiple rules, dates, exclusion rules, and exclusion dates. The type
|
| 1310 |
+
constructor takes the following keyword arguments:
|
| 1311 |
+
|
| 1312 |
+
:param cache: If True, caching of results will be enabled, improving
|
| 1313 |
+
performance of multiple queries considerably. """
|
| 1314 |
+
|
| 1315 |
+
class _genitem(object):
|
| 1316 |
+
def __init__(self, genlist, gen):
|
| 1317 |
+
try:
|
| 1318 |
+
self.dt = advance_iterator(gen)
|
| 1319 |
+
genlist.append(self)
|
| 1320 |
+
except StopIteration:
|
| 1321 |
+
pass
|
| 1322 |
+
self.genlist = genlist
|
| 1323 |
+
self.gen = gen
|
| 1324 |
+
|
| 1325 |
+
def __next__(self):
|
| 1326 |
+
try:
|
| 1327 |
+
self.dt = advance_iterator(self.gen)
|
| 1328 |
+
except StopIteration:
|
| 1329 |
+
if self.genlist[0] is self:
|
| 1330 |
+
heapq.heappop(self.genlist)
|
| 1331 |
+
else:
|
| 1332 |
+
self.genlist.remove(self)
|
| 1333 |
+
heapq.heapify(self.genlist)
|
| 1334 |
+
|
| 1335 |
+
next = __next__
|
| 1336 |
+
|
| 1337 |
+
def __lt__(self, other):
|
| 1338 |
+
return self.dt < other.dt
|
| 1339 |
+
|
| 1340 |
+
def __gt__(self, other):
|
| 1341 |
+
return self.dt > other.dt
|
| 1342 |
+
|
| 1343 |
+
def __eq__(self, other):
|
| 1344 |
+
return self.dt == other.dt
|
| 1345 |
+
|
| 1346 |
+
def __ne__(self, other):
|
| 1347 |
+
return self.dt != other.dt
|
| 1348 |
+
|
| 1349 |
+
def __init__(self, cache=False):
|
| 1350 |
+
super(rruleset, self).__init__(cache)
|
| 1351 |
+
self._rrule = []
|
| 1352 |
+
self._rdate = []
|
| 1353 |
+
self._exrule = []
|
| 1354 |
+
self._exdate = []
|
| 1355 |
+
|
| 1356 |
+
@_invalidates_cache
|
| 1357 |
+
def rrule(self, rrule):
|
| 1358 |
+
""" Include the given :py:class:`rrule` instance in the recurrence set
|
| 1359 |
+
generation. """
|
| 1360 |
+
self._rrule.append(rrule)
|
| 1361 |
+
|
| 1362 |
+
@_invalidates_cache
|
| 1363 |
+
def rdate(self, rdate):
|
| 1364 |
+
""" Include the given :py:class:`datetime` instance in the recurrence
|
| 1365 |
+
set generation. """
|
| 1366 |
+
self._rdate.append(rdate)
|
| 1367 |
+
|
| 1368 |
+
@_invalidates_cache
|
| 1369 |
+
def exrule(self, exrule):
|
| 1370 |
+
""" Include the given rrule instance in the recurrence set exclusion
|
| 1371 |
+
list. Dates which are part of the given recurrence rules will not
|
| 1372 |
+
be generated, even if some inclusive rrule or rdate matches them.
|
| 1373 |
+
"""
|
| 1374 |
+
self._exrule.append(exrule)
|
| 1375 |
+
|
| 1376 |
+
@_invalidates_cache
|
| 1377 |
+
def exdate(self, exdate):
|
| 1378 |
+
""" Include the given datetime instance in the recurrence set
|
| 1379 |
+
exclusion list. Dates included that way will not be generated,
|
| 1380 |
+
even if some inclusive rrule or rdate matches them. """
|
| 1381 |
+
self._exdate.append(exdate)
|
| 1382 |
+
|
| 1383 |
+
def _iter(self):
|
| 1384 |
+
rlist = []
|
| 1385 |
+
self._rdate.sort()
|
| 1386 |
+
self._genitem(rlist, iter(self._rdate))
|
| 1387 |
+
for gen in [iter(x) for x in self._rrule]:
|
| 1388 |
+
self._genitem(rlist, gen)
|
| 1389 |
+
exlist = []
|
| 1390 |
+
self._exdate.sort()
|
| 1391 |
+
self._genitem(exlist, iter(self._exdate))
|
| 1392 |
+
for gen in [iter(x) for x in self._exrule]:
|
| 1393 |
+
self._genitem(exlist, gen)
|
| 1394 |
+
lastdt = None
|
| 1395 |
+
total = 0
|
| 1396 |
+
heapq.heapify(rlist)
|
| 1397 |
+
heapq.heapify(exlist)
|
| 1398 |
+
while rlist:
|
| 1399 |
+
ritem = rlist[0]
|
| 1400 |
+
if not lastdt or lastdt != ritem.dt:
|
| 1401 |
+
while exlist and exlist[0] < ritem:
|
| 1402 |
+
exitem = exlist[0]
|
| 1403 |
+
advance_iterator(exitem)
|
| 1404 |
+
if exlist and exlist[0] is exitem:
|
| 1405 |
+
heapq.heapreplace(exlist, exitem)
|
| 1406 |
+
if not exlist or ritem != exlist[0]:
|
| 1407 |
+
total += 1
|
| 1408 |
+
yield ritem.dt
|
| 1409 |
+
lastdt = ritem.dt
|
| 1410 |
+
advance_iterator(ritem)
|
| 1411 |
+
if rlist and rlist[0] is ritem:
|
| 1412 |
+
heapq.heapreplace(rlist, ritem)
|
| 1413 |
+
self._len = total
|
| 1414 |
+
|
| 1415 |
+
|
| 1416 |
+
|
| 1417 |
+
|
| 1418 |
+
class _rrulestr(object):
|
| 1419 |
+
""" Parses a string representation of a recurrence rule or set of
|
| 1420 |
+
recurrence rules.
|
| 1421 |
+
|
| 1422 |
+
:param s:
|
| 1423 |
+
Required, a string defining one or more recurrence rules.
|
| 1424 |
+
|
| 1425 |
+
:param dtstart:
|
| 1426 |
+
If given, used as the default recurrence start if not specified in the
|
| 1427 |
+
rule string.
|
| 1428 |
+
|
| 1429 |
+
:param cache:
|
| 1430 |
+
If set ``True`` caching of results will be enabled, improving
|
| 1431 |
+
performance of multiple queries considerably.
|
| 1432 |
+
|
| 1433 |
+
:param unfold:
|
| 1434 |
+
If set ``True`` indicates that a rule string is split over more
|
| 1435 |
+
than one line and should be joined before processing.
|
| 1436 |
+
|
| 1437 |
+
:param forceset:
|
| 1438 |
+
If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
|
| 1439 |
+
be returned.
|
| 1440 |
+
|
| 1441 |
+
:param compatible:
|
| 1442 |
+
If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
|
| 1443 |
+
|
| 1444 |
+
:param ignoretz:
|
| 1445 |
+
If set ``True``, time zones in parsed strings are ignored and a naive
|
| 1446 |
+
:class:`datetime.datetime` object is returned.
|
| 1447 |
+
|
| 1448 |
+
:param tzids:
|
| 1449 |
+
If given, a callable or mapping used to retrieve a
|
| 1450 |
+
:class:`datetime.tzinfo` from a string representation.
|
| 1451 |
+
Defaults to :func:`dateutil.tz.gettz`.
|
| 1452 |
+
|
| 1453 |
+
:param tzinfos:
|
| 1454 |
+
Additional time zone names / aliases which may be present in a string
|
| 1455 |
+
representation. See :func:`dateutil.parser.parse` for more
|
| 1456 |
+
information.
|
| 1457 |
+
|
| 1458 |
+
:return:
|
| 1459 |
+
Returns a :class:`dateutil.rrule.rruleset` or
|
| 1460 |
+
:class:`dateutil.rrule.rrule`
|
| 1461 |
+
"""
|
| 1462 |
+
|
| 1463 |
+
_freq_map = {"YEARLY": YEARLY,
|
| 1464 |
+
"MONTHLY": MONTHLY,
|
| 1465 |
+
"WEEKLY": WEEKLY,
|
| 1466 |
+
"DAILY": DAILY,
|
| 1467 |
+
"HOURLY": HOURLY,
|
| 1468 |
+
"MINUTELY": MINUTELY,
|
| 1469 |
+
"SECONDLY": SECONDLY}
|
| 1470 |
+
|
| 1471 |
+
_weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
|
| 1472 |
+
"FR": 4, "SA": 5, "SU": 6}
|
| 1473 |
+
|
| 1474 |
+
def _handle_int(self, rrkwargs, name, value, **kwargs):
|
| 1475 |
+
rrkwargs[name.lower()] = int(value)
|
| 1476 |
+
|
| 1477 |
+
def _handle_int_list(self, rrkwargs, name, value, **kwargs):
|
| 1478 |
+
rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
|
| 1479 |
+
|
| 1480 |
+
_handle_INTERVAL = _handle_int
|
| 1481 |
+
_handle_COUNT = _handle_int
|
| 1482 |
+
_handle_BYSETPOS = _handle_int_list
|
| 1483 |
+
_handle_BYMONTH = _handle_int_list
|
| 1484 |
+
_handle_BYMONTHDAY = _handle_int_list
|
| 1485 |
+
_handle_BYYEARDAY = _handle_int_list
|
| 1486 |
+
_handle_BYEASTER = _handle_int_list
|
| 1487 |
+
_handle_BYWEEKNO = _handle_int_list
|
| 1488 |
+
_handle_BYHOUR = _handle_int_list
|
| 1489 |
+
_handle_BYMINUTE = _handle_int_list
|
| 1490 |
+
_handle_BYSECOND = _handle_int_list
|
| 1491 |
+
|
| 1492 |
+
def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
|
| 1493 |
+
rrkwargs["freq"] = self._freq_map[value]
|
| 1494 |
+
|
| 1495 |
+
def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
|
| 1496 |
+
global parser
|
| 1497 |
+
if not parser:
|
| 1498 |
+
from dateutil import parser
|
| 1499 |
+
try:
|
| 1500 |
+
rrkwargs["until"] = parser.parse(value,
|
| 1501 |
+
ignoretz=kwargs.get("ignoretz"),
|
| 1502 |
+
tzinfos=kwargs.get("tzinfos"))
|
| 1503 |
+
except ValueError:
|
| 1504 |
+
raise ValueError("invalid until date")
|
| 1505 |
+
|
| 1506 |
+
def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
| 1507 |
+
rrkwargs["wkst"] = self._weekday_map[value]
|
| 1508 |
+
|
| 1509 |
+
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
|
| 1510 |
+
"""
|
| 1511 |
+
Two ways to specify this: +1MO or MO(+1)
|
| 1512 |
+
"""
|
| 1513 |
+
l = []
|
| 1514 |
+
for wday in value.split(','):
|
| 1515 |
+
if '(' in wday:
|
| 1516 |
+
# If it's of the form TH(+1), etc.
|
| 1517 |
+
splt = wday.split('(')
|
| 1518 |
+
w = splt[0]
|
| 1519 |
+
n = int(splt[1][:-1])
|
| 1520 |
+
elif len(wday):
|
| 1521 |
+
# If it's of the form +1MO
|
| 1522 |
+
for i in range(len(wday)):
|
| 1523 |
+
if wday[i] not in '+-0123456789':
|
| 1524 |
+
break
|
| 1525 |
+
n = wday[:i] or None
|
| 1526 |
+
w = wday[i:]
|
| 1527 |
+
if n:
|
| 1528 |
+
n = int(n)
|
| 1529 |
+
else:
|
| 1530 |
+
raise ValueError("Invalid (empty) BYDAY specification.")
|
| 1531 |
+
|
| 1532 |
+
l.append(weekdays[self._weekday_map[w]](n))
|
| 1533 |
+
rrkwargs["byweekday"] = l
|
| 1534 |
+
|
| 1535 |
+
_handle_BYDAY = _handle_BYWEEKDAY
|
| 1536 |
+
|
| 1537 |
+
def _parse_rfc_rrule(self, line,
|
| 1538 |
+
dtstart=None,
|
| 1539 |
+
cache=False,
|
| 1540 |
+
ignoretz=False,
|
| 1541 |
+
tzinfos=None):
|
| 1542 |
+
if line.find(':') != -1:
|
| 1543 |
+
name, value = line.split(':')
|
| 1544 |
+
if name != "RRULE":
|
| 1545 |
+
raise ValueError("unknown parameter name")
|
| 1546 |
+
else:
|
| 1547 |
+
value = line
|
| 1548 |
+
rrkwargs = {}
|
| 1549 |
+
for pair in value.split(';'):
|
| 1550 |
+
name, value = pair.split('=')
|
| 1551 |
+
name = name.upper()
|
| 1552 |
+
value = value.upper()
|
| 1553 |
+
try:
|
| 1554 |
+
getattr(self, "_handle_"+name)(rrkwargs, name, value,
|
| 1555 |
+
ignoretz=ignoretz,
|
| 1556 |
+
tzinfos=tzinfos)
|
| 1557 |
+
except AttributeError:
|
| 1558 |
+
raise ValueError("unknown parameter '%s'" % name)
|
| 1559 |
+
except (KeyError, ValueError):
|
| 1560 |
+
raise ValueError("invalid '%s': %s" % (name, value))
|
| 1561 |
+
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
|
| 1562 |
+
|
| 1563 |
+
def _parse_date_value(self, date_value, parms, rule_tzids,
|
| 1564 |
+
ignoretz, tzids, tzinfos):
|
| 1565 |
+
global parser
|
| 1566 |
+
if not parser:
|
| 1567 |
+
from dateutil import parser
|
| 1568 |
+
|
| 1569 |
+
datevals = []
|
| 1570 |
+
value_found = False
|
| 1571 |
+
TZID = None
|
| 1572 |
+
|
| 1573 |
+
for parm in parms:
|
| 1574 |
+
if parm.startswith("TZID="):
|
| 1575 |
+
try:
|
| 1576 |
+
tzkey = rule_tzids[parm.split('TZID=')[-1]]
|
| 1577 |
+
except KeyError:
|
| 1578 |
+
continue
|
| 1579 |
+
if tzids is None:
|
| 1580 |
+
from . import tz
|
| 1581 |
+
tzlookup = tz.gettz
|
| 1582 |
+
elif callable(tzids):
|
| 1583 |
+
tzlookup = tzids
|
| 1584 |
+
else:
|
| 1585 |
+
tzlookup = getattr(tzids, 'get', None)
|
| 1586 |
+
if tzlookup is None:
|
| 1587 |
+
msg = ('tzids must be a callable, mapping, or None, '
|
| 1588 |
+
'not %s' % tzids)
|
| 1589 |
+
raise ValueError(msg)
|
| 1590 |
+
|
| 1591 |
+
TZID = tzlookup(tzkey)
|
| 1592 |
+
continue
|
| 1593 |
+
|
| 1594 |
+
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
|
| 1595 |
+
# only once.
|
| 1596 |
+
if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
|
| 1597 |
+
raise ValueError("unsupported parm: " + parm)
|
| 1598 |
+
else:
|
| 1599 |
+
if value_found:
|
| 1600 |
+
msg = ("Duplicate value parameter found in: " + parm)
|
| 1601 |
+
raise ValueError(msg)
|
| 1602 |
+
value_found = True
|
| 1603 |
+
|
| 1604 |
+
for datestr in date_value.split(','):
|
| 1605 |
+
date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
|
| 1606 |
+
if TZID is not None:
|
| 1607 |
+
if date.tzinfo is None:
|
| 1608 |
+
date = date.replace(tzinfo=TZID)
|
| 1609 |
+
else:
|
| 1610 |
+
raise ValueError('DTSTART/EXDATE specifies multiple timezone')
|
| 1611 |
+
datevals.append(date)
|
| 1612 |
+
|
| 1613 |
+
return datevals
|
| 1614 |
+
|
| 1615 |
+
def _parse_rfc(self, s,
|
| 1616 |
+
dtstart=None,
|
| 1617 |
+
cache=False,
|
| 1618 |
+
unfold=False,
|
| 1619 |
+
forceset=False,
|
| 1620 |
+
compatible=False,
|
| 1621 |
+
ignoretz=False,
|
| 1622 |
+
tzids=None,
|
| 1623 |
+
tzinfos=None):
|
| 1624 |
+
global parser
|
| 1625 |
+
if compatible:
|
| 1626 |
+
forceset = True
|
| 1627 |
+
unfold = True
|
| 1628 |
+
|
| 1629 |
+
TZID_NAMES = dict(map(
|
| 1630 |
+
lambda x: (x.upper(), x),
|
| 1631 |
+
re.findall('TZID=(?P<name>[^:]+):', s)
|
| 1632 |
+
))
|
| 1633 |
+
s = s.upper()
|
| 1634 |
+
if not s.strip():
|
| 1635 |
+
raise ValueError("empty string")
|
| 1636 |
+
if unfold:
|
| 1637 |
+
lines = s.splitlines()
|
| 1638 |
+
i = 0
|
| 1639 |
+
while i < len(lines):
|
| 1640 |
+
line = lines[i].rstrip()
|
| 1641 |
+
if not line:
|
| 1642 |
+
del lines[i]
|
| 1643 |
+
elif i > 0 and line[0] == " ":
|
| 1644 |
+
lines[i-1] += line[1:]
|
| 1645 |
+
del lines[i]
|
| 1646 |
+
else:
|
| 1647 |
+
i += 1
|
| 1648 |
+
else:
|
| 1649 |
+
lines = s.split()
|
| 1650 |
+
if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
|
| 1651 |
+
s.startswith('RRULE:'))):
|
| 1652 |
+
return self._parse_rfc_rrule(lines[0], cache=cache,
|
| 1653 |
+
dtstart=dtstart, ignoretz=ignoretz,
|
| 1654 |
+
tzinfos=tzinfos)
|
| 1655 |
+
else:
|
| 1656 |
+
rrulevals = []
|
| 1657 |
+
rdatevals = []
|
| 1658 |
+
exrulevals = []
|
| 1659 |
+
exdatevals = []
|
| 1660 |
+
for line in lines:
|
| 1661 |
+
if not line:
|
| 1662 |
+
continue
|
| 1663 |
+
if line.find(':') == -1:
|
| 1664 |
+
name = "RRULE"
|
| 1665 |
+
value = line
|
| 1666 |
+
else:
|
| 1667 |
+
name, value = line.split(':', 1)
|
| 1668 |
+
parms = name.split(';')
|
| 1669 |
+
if not parms:
|
| 1670 |
+
raise ValueError("empty property name")
|
| 1671 |
+
name = parms[0]
|
| 1672 |
+
parms = parms[1:]
|
| 1673 |
+
if name == "RRULE":
|
| 1674 |
+
for parm in parms:
|
| 1675 |
+
raise ValueError("unsupported RRULE parm: "+parm)
|
| 1676 |
+
rrulevals.append(value)
|
| 1677 |
+
elif name == "RDATE":
|
| 1678 |
+
for parm in parms:
|
| 1679 |
+
if parm != "VALUE=DATE-TIME":
|
| 1680 |
+
raise ValueError("unsupported RDATE parm: "+parm)
|
| 1681 |
+
rdatevals.append(value)
|
| 1682 |
+
elif name == "EXRULE":
|
| 1683 |
+
for parm in parms:
|
| 1684 |
+
raise ValueError("unsupported EXRULE parm: "+parm)
|
| 1685 |
+
exrulevals.append(value)
|
| 1686 |
+
elif name == "EXDATE":
|
| 1687 |
+
exdatevals.extend(
|
| 1688 |
+
self._parse_date_value(value, parms,
|
| 1689 |
+
TZID_NAMES, ignoretz,
|
| 1690 |
+
tzids, tzinfos)
|
| 1691 |
+
)
|
| 1692 |
+
elif name == "DTSTART":
|
| 1693 |
+
dtvals = self._parse_date_value(value, parms, TZID_NAMES,
|
| 1694 |
+
ignoretz, tzids, tzinfos)
|
| 1695 |
+
if len(dtvals) != 1:
|
| 1696 |
+
raise ValueError("Multiple DTSTART values specified:" +
|
| 1697 |
+
value)
|
| 1698 |
+
dtstart = dtvals[0]
|
| 1699 |
+
else:
|
| 1700 |
+
raise ValueError("unsupported property: "+name)
|
| 1701 |
+
if (forceset or len(rrulevals) > 1 or rdatevals
|
| 1702 |
+
or exrulevals or exdatevals):
|
| 1703 |
+
if not parser and (rdatevals or exdatevals):
|
| 1704 |
+
from dateutil import parser
|
| 1705 |
+
rset = rruleset(cache=cache)
|
| 1706 |
+
for value in rrulevals:
|
| 1707 |
+
rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
| 1708 |
+
ignoretz=ignoretz,
|
| 1709 |
+
tzinfos=tzinfos))
|
| 1710 |
+
for value in rdatevals:
|
| 1711 |
+
for datestr in value.split(','):
|
| 1712 |
+
rset.rdate(parser.parse(datestr,
|
| 1713 |
+
ignoretz=ignoretz,
|
| 1714 |
+
tzinfos=tzinfos))
|
| 1715 |
+
for value in exrulevals:
|
| 1716 |
+
rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
| 1717 |
+
ignoretz=ignoretz,
|
| 1718 |
+
tzinfos=tzinfos))
|
| 1719 |
+
for value in exdatevals:
|
| 1720 |
+
rset.exdate(value)
|
| 1721 |
+
if compatible and dtstart:
|
| 1722 |
+
rset.rdate(dtstart)
|
| 1723 |
+
return rset
|
| 1724 |
+
else:
|
| 1725 |
+
return self._parse_rfc_rrule(rrulevals[0],
|
| 1726 |
+
dtstart=dtstart,
|
| 1727 |
+
cache=cache,
|
| 1728 |
+
ignoretz=ignoretz,
|
| 1729 |
+
tzinfos=tzinfos)
|
| 1730 |
+
|
| 1731 |
+
def __call__(self, s, **kwargs):
|
| 1732 |
+
return self._parse_rfc(s, **kwargs)
|
| 1733 |
+
|
| 1734 |
+
|
| 1735 |
+
rrulestr = _rrulestr()
|
| 1736 |
+
|
| 1737 |
+
# vim:ts=4:sw=4:et
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/tzwin.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tzwin has moved to dateutil.tz.win
|
| 2 |
+
from .tz.win import *
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/dateutil/utils.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers general convenience and utility functions for dealing with
|
| 4 |
+
datetimes.
|
| 5 |
+
|
| 6 |
+
.. versionadded:: 2.7.0
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import unicode_literals
|
| 9 |
+
|
| 10 |
+
from datetime import datetime, time
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def today(tzinfo=None):
|
| 14 |
+
"""
|
| 15 |
+
Returns a :py:class:`datetime` representing the current day at midnight
|
| 16 |
+
|
| 17 |
+
:param tzinfo:
|
| 18 |
+
The time zone to attach (also used to determine the current day).
|
| 19 |
+
|
| 20 |
+
:return:
|
| 21 |
+
A :py:class:`datetime.datetime` object representing the current day
|
| 22 |
+
at midnight.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
dt = datetime.now(tzinfo)
|
| 26 |
+
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def default_tzinfo(dt, tzinfo):
|
| 30 |
+
"""
|
| 31 |
+
Sets the ``tzinfo`` parameter on naive datetimes only
|
| 32 |
+
|
| 33 |
+
This is useful for example when you are provided a datetime that may have
|
| 34 |
+
either an implicit or explicit time zone, such as when parsing a time zone
|
| 35 |
+
string.
|
| 36 |
+
|
| 37 |
+
.. doctest::
|
| 38 |
+
|
| 39 |
+
>>> from dateutil.tz import tzoffset
|
| 40 |
+
>>> from dateutil.parser import parse
|
| 41 |
+
>>> from dateutil.utils import default_tzinfo
|
| 42 |
+
>>> dflt_tz = tzoffset("EST", -18000)
|
| 43 |
+
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
| 44 |
+
2014-01-01 12:30:00+00:00
|
| 45 |
+
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
| 46 |
+
2014-01-01 12:30:00-05:00
|
| 47 |
+
|
| 48 |
+
:param dt:
|
| 49 |
+
The datetime on which to replace the time zone
|
| 50 |
+
|
| 51 |
+
:param tzinfo:
|
| 52 |
+
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
| 53 |
+
``dt`` if (and only if) it is naive.
|
| 54 |
+
|
| 55 |
+
:return:
|
| 56 |
+
Returns an aware :py:class:`datetime.datetime`.
|
| 57 |
+
"""
|
| 58 |
+
if dt.tzinfo is not None:
|
| 59 |
+
return dt
|
| 60 |
+
else:
|
| 61 |
+
return dt.replace(tzinfo=tzinfo)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def within_delta(dt1, dt2, delta):
|
| 65 |
+
"""
|
| 66 |
+
Useful for comparing two datetimes that may have a negligible difference
|
| 67 |
+
to be considered equal.
|
| 68 |
+
"""
|
| 69 |
+
delta = abs(delta)
|
| 70 |
+
difference = dt1 - dt2
|
| 71 |
+
return -delta <= difference <= delta
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
from importlib.metadata import entry_points
|
| 3 |
+
except ImportError: # python < 3.8
|
| 4 |
+
try:
|
| 5 |
+
from importlib_metadata import entry_points
|
| 6 |
+
except ImportError:
|
| 7 |
+
entry_points = None
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
from . import _version, caching
|
| 11 |
+
from .callbacks import Callback
|
| 12 |
+
from .compression import available_compressions
|
| 13 |
+
from .core import get_fs_token_paths, open, open_files, open_local
|
| 14 |
+
from .exceptions import FSTimeoutError
|
| 15 |
+
from .mapping import FSMap, get_mapper
|
| 16 |
+
from .registry import (
|
| 17 |
+
available_protocols,
|
| 18 |
+
filesystem,
|
| 19 |
+
get_filesystem_class,
|
| 20 |
+
register_implementation,
|
| 21 |
+
registry,
|
| 22 |
+
)
|
| 23 |
+
from .spec import AbstractFileSystem
|
| 24 |
+
|
| 25 |
+
__version__ = _version.get_versions()["version"]
|
| 26 |
+
|
| 27 |
+
__all__ = [
|
| 28 |
+
"AbstractFileSystem",
|
| 29 |
+
"FSTimeoutError",
|
| 30 |
+
"FSMap",
|
| 31 |
+
"filesystem",
|
| 32 |
+
"register_implementation",
|
| 33 |
+
"get_filesystem_class",
|
| 34 |
+
"get_fs_token_paths",
|
| 35 |
+
"get_mapper",
|
| 36 |
+
"open",
|
| 37 |
+
"open_files",
|
| 38 |
+
"open_local",
|
| 39 |
+
"registry",
|
| 40 |
+
"caching",
|
| 41 |
+
"Callback",
|
| 42 |
+
"available_protocols",
|
| 43 |
+
"available_compressions",
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def process_entries():
|
| 48 |
+
if entry_points is not None:
|
| 49 |
+
try:
|
| 50 |
+
eps = entry_points()
|
| 51 |
+
except TypeError:
|
| 52 |
+
pass # importlib-metadata < 0.8
|
| 53 |
+
else:
|
| 54 |
+
if hasattr(eps, "select"): # Python 3.10+ / importlib_metadata >= 3.9.0
|
| 55 |
+
specs = eps.select(group="fsspec.specs")
|
| 56 |
+
else:
|
| 57 |
+
specs = eps.get("fsspec.specs", [])
|
| 58 |
+
for spec in specs:
|
| 59 |
+
err_msg = f"Unable to load filesystem from {spec}"
|
| 60 |
+
register_implementation(
|
| 61 |
+
spec.name, spec.value.replace(":", "."), errtxt=err_msg
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
process_entries()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# This file was generated by 'versioneer.py' (0.20) from
|
| 3 |
+
# revision-control system data, or from the parent directory name of an
|
| 4 |
+
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
| 5 |
+
# of this file.
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
version_json = '''
|
| 10 |
+
{
|
| 11 |
+
"date": "2022-05-19T14:13:38-0400",
|
| 12 |
+
"dirty": false,
|
| 13 |
+
"error": null,
|
| 14 |
+
"full-revisionid": "148a6861481f824afb88c7c50955aa6ed4e25d32",
|
| 15 |
+
"version": "2022.5.0"
|
| 16 |
+
}
|
| 17 |
+
''' # END VERSION_JSON
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_versions():
|
| 21 |
+
return json.loads(version_json)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/archive.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fsspec import AbstractFileSystem
|
| 2 |
+
from fsspec.utils import tokenize
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class AbstractArchiveFileSystem(AbstractFileSystem):
|
| 6 |
+
"""
|
| 7 |
+
A generic superclass for implementing Archive-based filesystems.
|
| 8 |
+
|
| 9 |
+
Currently, it is shared amongst `ZipFileSystem`, `LibArchiveFileSystem` and
|
| 10 |
+
`TarFileSystem`.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __str__(self):
|
| 14 |
+
return "<Archive-like object %s at %s>" % (type(self).__name__, id(self))
|
| 15 |
+
|
| 16 |
+
__repr__ = __str__
|
| 17 |
+
|
| 18 |
+
def ukey(self, path):
|
| 19 |
+
return tokenize(path, self.fo, self.protocol)
|
| 20 |
+
|
| 21 |
+
def _all_dirnames(self, paths):
|
| 22 |
+
"""Returns *all* directory names for each path in paths, including intermediate ones.
|
| 23 |
+
|
| 24 |
+
Parameters
|
| 25 |
+
----------
|
| 26 |
+
paths: Iterable of path strings
|
| 27 |
+
"""
|
| 28 |
+
if len(paths) == 0:
|
| 29 |
+
return set()
|
| 30 |
+
|
| 31 |
+
dirnames = {self._parent(path) for path in paths} - {self.root_marker}
|
| 32 |
+
return dirnames | self._all_dirnames(dirnames)
|
| 33 |
+
|
| 34 |
+
def info(self, path, **kwargs):
|
| 35 |
+
self._get_dirs()
|
| 36 |
+
path = self._strip_protocol(path)
|
| 37 |
+
if path in self.dir_cache:
|
| 38 |
+
return self.dir_cache[path]
|
| 39 |
+
elif path + "/" in self.dir_cache:
|
| 40 |
+
return self.dir_cache[path + "/"]
|
| 41 |
+
else:
|
| 42 |
+
raise FileNotFoundError(path)
|
| 43 |
+
|
| 44 |
+
def ls(self, path, detail=False, **kwargs):
|
| 45 |
+
self._get_dirs()
|
| 46 |
+
paths = {}
|
| 47 |
+
for p, f in self.dir_cache.items():
|
| 48 |
+
p = p.rstrip("/")
|
| 49 |
+
if "/" in p:
|
| 50 |
+
root = p.rsplit("/", 1)[0]
|
| 51 |
+
else:
|
| 52 |
+
root = ""
|
| 53 |
+
if root == path.rstrip("/"):
|
| 54 |
+
paths[p] = f
|
| 55 |
+
elif all(
|
| 56 |
+
(a == b)
|
| 57 |
+
for a, b in zip(path.split("/"), [""] + p.strip("/").split("/"))
|
| 58 |
+
):
|
| 59 |
+
# root directory entry
|
| 60 |
+
ppath = p.rstrip("/").split("/", 1)[0]
|
| 61 |
+
if ppath not in paths:
|
| 62 |
+
out = {"name": ppath + "/", "size": 0, "type": "directory"}
|
| 63 |
+
paths[ppath] = out
|
| 64 |
+
out = list(paths.values())
|
| 65 |
+
if detail:
|
| 66 |
+
return out
|
| 67 |
+
else:
|
| 68 |
+
return list(sorted(f["name"] for f in out))
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/asyn.py
ADDED
|
@@ -0,0 +1,933 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import asyncio.events
|
| 3 |
+
import functools
|
| 4 |
+
import inspect
|
| 5 |
+
import io
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
import threading
|
| 10 |
+
from contextlib import contextmanager
|
| 11 |
+
from glob import has_magic
|
| 12 |
+
|
| 13 |
+
from .callbacks import _DEFAULT_CALLBACK
|
| 14 |
+
from .exceptions import FSTimeoutError
|
| 15 |
+
from .spec import AbstractBufferedFile, AbstractFileSystem
|
| 16 |
+
from .utils import is_exception, other_paths
|
| 17 |
+
|
| 18 |
+
private = re.compile("_[^_]")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
async def _runner(event, coro, result, timeout=None):
|
| 22 |
+
timeout = timeout if timeout else None # convert 0 or 0.0 to None
|
| 23 |
+
if timeout is not None:
|
| 24 |
+
coro = asyncio.wait_for(coro, timeout=timeout)
|
| 25 |
+
try:
|
| 26 |
+
result[0] = await coro
|
| 27 |
+
except Exception as ex:
|
| 28 |
+
result[0] = ex
|
| 29 |
+
finally:
|
| 30 |
+
event.set()
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def sync(loop, func, *args, timeout=None, **kwargs):
|
| 34 |
+
"""
|
| 35 |
+
Make loop run coroutine until it returns. Runs in other thread
|
| 36 |
+
"""
|
| 37 |
+
timeout = timeout if timeout else None # convert 0 or 0.0 to None
|
| 38 |
+
# NB: if the loop is not running *yet*, it is OK to submit work
|
| 39 |
+
# and we will wait for it
|
| 40 |
+
if loop is None or loop.is_closed():
|
| 41 |
+
raise RuntimeError("Loop is not running")
|
| 42 |
+
try:
|
| 43 |
+
loop0 = asyncio.events.get_running_loop()
|
| 44 |
+
if loop0 is loop:
|
| 45 |
+
raise NotImplementedError("Calling sync() from within a running loop")
|
| 46 |
+
except RuntimeError:
|
| 47 |
+
pass
|
| 48 |
+
coro = func(*args, **kwargs)
|
| 49 |
+
result = [None]
|
| 50 |
+
event = threading.Event()
|
| 51 |
+
asyncio.run_coroutine_threadsafe(_runner(event, coro, result, timeout), loop)
|
| 52 |
+
while True:
|
| 53 |
+
# this loops allows thread to get interrupted
|
| 54 |
+
if event.wait(1):
|
| 55 |
+
break
|
| 56 |
+
if timeout is not None:
|
| 57 |
+
timeout -= 1
|
| 58 |
+
if timeout < 0:
|
| 59 |
+
raise FSTimeoutError
|
| 60 |
+
|
| 61 |
+
return_result = result[0]
|
| 62 |
+
if isinstance(return_result, asyncio.TimeoutError):
|
| 63 |
+
# suppress asyncio.TimeoutError, raise FSTimeoutError
|
| 64 |
+
raise FSTimeoutError from return_result
|
| 65 |
+
elif isinstance(return_result, BaseException):
|
| 66 |
+
raise return_result
|
| 67 |
+
else:
|
| 68 |
+
return return_result
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
iothread = [None] # dedicated fsspec IO thread
|
| 72 |
+
loop = [None] # global event loop for any non-async instance
|
| 73 |
+
lock = threading.Lock() # for setting exactly one thread
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def sync_wrapper(func, obj=None):
|
| 77 |
+
"""Given a function, make so can be called in async or blocking contexts
|
| 78 |
+
|
| 79 |
+
Leave obj=None if defining within a class. Pass the instance if attaching
|
| 80 |
+
as an attribute of the instance.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
@functools.wraps(func)
|
| 84 |
+
def wrapper(*args, **kwargs):
|
| 85 |
+
self = obj or args[0]
|
| 86 |
+
return sync(self.loop, func, *args, **kwargs)
|
| 87 |
+
|
| 88 |
+
return wrapper
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@contextmanager
|
| 92 |
+
def _selector_policy():
|
| 93 |
+
original_policy = asyncio.get_event_loop_policy()
|
| 94 |
+
try:
|
| 95 |
+
if (
|
| 96 |
+
sys.version_info >= (3, 8)
|
| 97 |
+
and os.name == "nt"
|
| 98 |
+
and hasattr(asyncio, "WindowsSelectorEventLoopPolicy")
|
| 99 |
+
):
|
| 100 |
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
| 101 |
+
|
| 102 |
+
yield
|
| 103 |
+
finally:
|
| 104 |
+
asyncio.set_event_loop_policy(original_policy)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def get_running_loop():
|
| 108 |
+
if hasattr(asyncio, "get_running_loop"):
|
| 109 |
+
return asyncio.get_running_loop()
|
| 110 |
+
else:
|
| 111 |
+
loop = asyncio._get_running_loop()
|
| 112 |
+
if loop is None:
|
| 113 |
+
raise RuntimeError("no running event loop")
|
| 114 |
+
else:
|
| 115 |
+
return loop
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def get_loop():
|
| 119 |
+
"""Create or return the default fsspec IO loop
|
| 120 |
+
|
| 121 |
+
The loop will be running on a separate thread.
|
| 122 |
+
"""
|
| 123 |
+
if loop[0] is None:
|
| 124 |
+
with lock:
|
| 125 |
+
# repeat the check just in case the loop got filled between the
|
| 126 |
+
# previous two calls from another thread
|
| 127 |
+
if loop[0] is None:
|
| 128 |
+
with _selector_policy():
|
| 129 |
+
loop[0] = asyncio.new_event_loop()
|
| 130 |
+
th = threading.Thread(target=loop[0].run_forever, name="fsspecIO")
|
| 131 |
+
th.daemon = True
|
| 132 |
+
th.start()
|
| 133 |
+
iothread[0] = th
|
| 134 |
+
return loop[0]
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
@contextmanager
|
| 138 |
+
def fsspec_loop():
|
| 139 |
+
"""Temporarily switch the current event loop to the fsspec's
|
| 140 |
+
own loop, and then revert it back after the context gets
|
| 141 |
+
terminated.
|
| 142 |
+
"""
|
| 143 |
+
try:
|
| 144 |
+
original_loop = get_running_loop()
|
| 145 |
+
except RuntimeError:
|
| 146 |
+
original_loop = None
|
| 147 |
+
|
| 148 |
+
fsspec_loop = get_loop()
|
| 149 |
+
try:
|
| 150 |
+
asyncio._set_running_loop(fsspec_loop)
|
| 151 |
+
yield fsspec_loop
|
| 152 |
+
finally:
|
| 153 |
+
asyncio._set_running_loop(original_loop)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
import resource
|
| 158 |
+
except ImportError:
|
| 159 |
+
resource = None
|
| 160 |
+
ResourceError = OSError
|
| 161 |
+
else:
|
| 162 |
+
ResourceEror = resource.error
|
| 163 |
+
|
| 164 |
+
_DEFAULT_BATCH_SIZE = 128
|
| 165 |
+
_NOFILES_DEFAULT_BATCH_SIZE = 1280
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def _get_batch_size(nofiles=False):
|
| 169 |
+
from fsspec.config import conf
|
| 170 |
+
|
| 171 |
+
if nofiles:
|
| 172 |
+
if "nofiles_gather_batch_size" in conf:
|
| 173 |
+
return conf["nofiles_gather_batch_size"]
|
| 174 |
+
else:
|
| 175 |
+
if "gather_batch_size" in conf:
|
| 176 |
+
return conf["gather_batch_size"]
|
| 177 |
+
if nofiles:
|
| 178 |
+
return _NOFILES_DEFAULT_BATCH_SIZE
|
| 179 |
+
if resource is None:
|
| 180 |
+
return _DEFAULT_BATCH_SIZE
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
|
| 184 |
+
except (ImportError, ValueError, ResourceError):
|
| 185 |
+
return _DEFAULT_BATCH_SIZE
|
| 186 |
+
|
| 187 |
+
if soft_limit == resource.RLIM_INFINITY:
|
| 188 |
+
return -1
|
| 189 |
+
else:
|
| 190 |
+
return soft_limit // 8
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
async def _run_coros_in_chunks(
|
| 194 |
+
coros,
|
| 195 |
+
batch_size=None,
|
| 196 |
+
callback=_DEFAULT_CALLBACK,
|
| 197 |
+
timeout=None,
|
| 198 |
+
return_exceptions=False,
|
| 199 |
+
nofiles=False,
|
| 200 |
+
):
|
| 201 |
+
"""Run the given coroutines in chunks.
|
| 202 |
+
|
| 203 |
+
Parameters
|
| 204 |
+
----------
|
| 205 |
+
coros: list of coroutines to run
|
| 206 |
+
batch_size: int or None
|
| 207 |
+
Number of coroutines to submit/wait on simultaneously.
|
| 208 |
+
If -1, then it will not be any throttling. If
|
| 209 |
+
None, it will be inferred from _get_batch_size()
|
| 210 |
+
callback: fsspec.callbacks.Callback instance
|
| 211 |
+
Gets a relative_update when each coroutine completes
|
| 212 |
+
timeout: number or None
|
| 213 |
+
If given, each coroutine times out after this time. Note that, since
|
| 214 |
+
there are multiple batches, the total run time of this function will in
|
| 215 |
+
general be longer
|
| 216 |
+
return_exceptions: bool
|
| 217 |
+
Same meaning as in asyncio.gather
|
| 218 |
+
nofiles: bool
|
| 219 |
+
If inferring the batch_size, does this operation involve local files?
|
| 220 |
+
If yes, you normally expect smaller batches.
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
+
if batch_size is None:
|
| 224 |
+
batch_size = _get_batch_size(nofiles=nofiles)
|
| 225 |
+
|
| 226 |
+
if batch_size == -1:
|
| 227 |
+
batch_size = len(coros)
|
| 228 |
+
|
| 229 |
+
assert batch_size > 0
|
| 230 |
+
results = []
|
| 231 |
+
for start in range(0, len(coros), batch_size):
|
| 232 |
+
chunk = [
|
| 233 |
+
asyncio.Task(asyncio.wait_for(c, timeout=timeout))
|
| 234 |
+
for c in coros[start : start + batch_size]
|
| 235 |
+
]
|
| 236 |
+
if callback is not _DEFAULT_CALLBACK:
|
| 237 |
+
[
|
| 238 |
+
t.add_done_callback(lambda *_, **__: callback.relative_update(1))
|
| 239 |
+
for t in chunk
|
| 240 |
+
]
|
| 241 |
+
results.extend(
|
| 242 |
+
await asyncio.gather(*chunk, return_exceptions=return_exceptions),
|
| 243 |
+
)
|
| 244 |
+
return results
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
# these methods should be implemented as async by any async-able backend
|
| 248 |
+
async_methods = [
|
| 249 |
+
"_ls",
|
| 250 |
+
"_cat_file",
|
| 251 |
+
"_get_file",
|
| 252 |
+
"_put_file",
|
| 253 |
+
"_rm_file",
|
| 254 |
+
"_cp_file",
|
| 255 |
+
"_pipe_file",
|
| 256 |
+
"_expand_path",
|
| 257 |
+
"_info",
|
| 258 |
+
"_isfile",
|
| 259 |
+
"_isdir",
|
| 260 |
+
"_exists",
|
| 261 |
+
"_walk",
|
| 262 |
+
"_glob",
|
| 263 |
+
"_find",
|
| 264 |
+
"_du",
|
| 265 |
+
"_size",
|
| 266 |
+
"_mkdir",
|
| 267 |
+
"_makedirs",
|
| 268 |
+
]
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class AsyncFileSystem(AbstractFileSystem):
|
| 272 |
+
"""Async file operations, default implementations
|
| 273 |
+
|
| 274 |
+
Passes bulk operations to asyncio.gather for concurrent operation.
|
| 275 |
+
|
| 276 |
+
Implementations that have concurrent batch operations and/or async methods
|
| 277 |
+
should inherit from this class instead of AbstractFileSystem. Docstrings are
|
| 278 |
+
copied from the un-underscored method in AbstractFileSystem, if not given.
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
# note that methods do not have docstring here; they will be copied
|
| 282 |
+
# for _* methods and inferred for overridden methods.
|
| 283 |
+
|
| 284 |
+
async_impl = True
|
| 285 |
+
disable_throttling = False
|
| 286 |
+
|
| 287 |
+
def __init__(self, *args, asynchronous=False, loop=None, batch_size=None, **kwargs):
|
| 288 |
+
self.asynchronous = asynchronous
|
| 289 |
+
self._pid = os.getpid()
|
| 290 |
+
if not asynchronous:
|
| 291 |
+
self._loop = loop or get_loop()
|
| 292 |
+
else:
|
| 293 |
+
self._loop = None
|
| 294 |
+
self.batch_size = batch_size
|
| 295 |
+
super().__init__(*args, **kwargs)
|
| 296 |
+
|
| 297 |
+
@property
|
| 298 |
+
def loop(self):
|
| 299 |
+
if self._pid != os.getpid():
|
| 300 |
+
raise RuntimeError("This class is not fork-safe")
|
| 301 |
+
return self._loop
|
| 302 |
+
|
| 303 |
+
async def _rm_file(self, path, **kwargs):
|
| 304 |
+
raise NotImplementedError
|
| 305 |
+
|
| 306 |
+
async def _rm(self, path, recursive=False, batch_size=None, **kwargs):
|
| 307 |
+
# TODO: implement on_error
|
| 308 |
+
batch_size = batch_size or self.batch_size
|
| 309 |
+
path = await self._expand_path(path, recursive=recursive)
|
| 310 |
+
return await _run_coros_in_chunks(
|
| 311 |
+
[self._rm_file(p, **kwargs) for p in path],
|
| 312 |
+
batch_size=batch_size,
|
| 313 |
+
nofiles=True,
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
async def _cp_file(self, path1, path2, **kwargs):
|
| 317 |
+
raise NotImplementedError
|
| 318 |
+
|
| 319 |
+
async def _copy(
|
| 320 |
+
self,
|
| 321 |
+
path1,
|
| 322 |
+
path2,
|
| 323 |
+
recursive=False,
|
| 324 |
+
on_error=None,
|
| 325 |
+
maxdepth=None,
|
| 326 |
+
batch_size=None,
|
| 327 |
+
**kwargs,
|
| 328 |
+
):
|
| 329 |
+
if on_error is None and recursive:
|
| 330 |
+
on_error = "ignore"
|
| 331 |
+
elif on_error is None:
|
| 332 |
+
on_error = "raise"
|
| 333 |
+
|
| 334 |
+
paths = await self._expand_path(path1, maxdepth=maxdepth, recursive=recursive)
|
| 335 |
+
path2 = other_paths(paths, path2)
|
| 336 |
+
batch_size = batch_size or self.batch_size
|
| 337 |
+
coros = [self._cp_file(p1, p2, **kwargs) for p1, p2 in zip(paths, path2)]
|
| 338 |
+
result = await _run_coros_in_chunks(
|
| 339 |
+
coros, batch_size=batch_size, return_exceptions=True, nofiles=True
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
for ex in filter(is_exception, result):
|
| 343 |
+
if on_error == "ignore" and isinstance(ex, FileNotFoundError):
|
| 344 |
+
continue
|
| 345 |
+
raise ex
|
| 346 |
+
|
| 347 |
+
async def _pipe(self, path, value=None, batch_size=None, **kwargs):
|
| 348 |
+
if isinstance(path, str):
|
| 349 |
+
path = {path: value}
|
| 350 |
+
batch_size = batch_size or self.batch_size
|
| 351 |
+
return await _run_coros_in_chunks(
|
| 352 |
+
[self._pipe_file(k, v, **kwargs) for k, v in path.items()],
|
| 353 |
+
batch_size=batch_size,
|
| 354 |
+
nofiles=True,
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
async def _process_limits(self, url, start, end):
|
| 358 |
+
"""Helper for "Range"-based _cat_file"""
|
| 359 |
+
size = None
|
| 360 |
+
suff = False
|
| 361 |
+
if start is not None and start < 0:
|
| 362 |
+
# if start is negative and end None, end is the "suffix length"
|
| 363 |
+
if end is None:
|
| 364 |
+
end = -start
|
| 365 |
+
start = ""
|
| 366 |
+
suff = True
|
| 367 |
+
else:
|
| 368 |
+
size = size or (await self._info(url))["size"]
|
| 369 |
+
start = size + start
|
| 370 |
+
elif start is None:
|
| 371 |
+
start = 0
|
| 372 |
+
if not suff:
|
| 373 |
+
if end is not None and end < 0:
|
| 374 |
+
if start is not None:
|
| 375 |
+
size = size or (await self._info(url))["size"]
|
| 376 |
+
end = size + end
|
| 377 |
+
elif end is None:
|
| 378 |
+
end = ""
|
| 379 |
+
if isinstance(end, int):
|
| 380 |
+
end -= 1 # bytes range is inclusive
|
| 381 |
+
return "bytes=%s-%s" % (start, end)
|
| 382 |
+
|
| 383 |
+
async def _cat_file(self, path, start=None, end=None, **kwargs):
|
| 384 |
+
raise NotImplementedError
|
| 385 |
+
|
| 386 |
+
async def _cat(
|
| 387 |
+
self, path, recursive=False, on_error="raise", batch_size=None, **kwargs
|
| 388 |
+
):
|
| 389 |
+
paths = await self._expand_path(path, recursive=recursive)
|
| 390 |
+
coros = [self._cat_file(path, **kwargs) for path in paths]
|
| 391 |
+
batch_size = batch_size or self.batch_size
|
| 392 |
+
out = await _run_coros_in_chunks(
|
| 393 |
+
coros, batch_size=batch_size, nofiles=True, return_exceptions=True
|
| 394 |
+
)
|
| 395 |
+
if on_error == "raise":
|
| 396 |
+
ex = next(filter(is_exception, out), False)
|
| 397 |
+
if ex:
|
| 398 |
+
raise ex
|
| 399 |
+
if (
|
| 400 |
+
len(paths) > 1
|
| 401 |
+
or isinstance(path, list)
|
| 402 |
+
or paths[0] != self._strip_protocol(path)
|
| 403 |
+
):
|
| 404 |
+
return {
|
| 405 |
+
k: v
|
| 406 |
+
for k, v in zip(paths, out)
|
| 407 |
+
if on_error != "omit" or not is_exception(v)
|
| 408 |
+
}
|
| 409 |
+
else:
|
| 410 |
+
return out[0]
|
| 411 |
+
|
| 412 |
+
async def _cat_ranges(
|
| 413 |
+
self, paths, starts, ends, max_gap=None, batch_size=None, **kwargs
|
| 414 |
+
):
|
| 415 |
+
# TODO: on_error
|
| 416 |
+
if max_gap is not None:
|
| 417 |
+
# use utils.merge_offset_ranges
|
| 418 |
+
raise NotImplementedError
|
| 419 |
+
if not isinstance(paths, list):
|
| 420 |
+
raise TypeError
|
| 421 |
+
if not isinstance(starts, list):
|
| 422 |
+
starts = [starts] * len(paths)
|
| 423 |
+
if not isinstance(ends, list):
|
| 424 |
+
ends = [starts] * len(paths)
|
| 425 |
+
if len(starts) != len(paths) or len(ends) != len(paths):
|
| 426 |
+
raise ValueError
|
| 427 |
+
coros = [
|
| 428 |
+
self._cat_file(p, start=s, end=e, **kwargs)
|
| 429 |
+
for p, s, e in zip(paths, starts, ends)
|
| 430 |
+
]
|
| 431 |
+
batch_size = batch_size or self.batch_size
|
| 432 |
+
return await _run_coros_in_chunks(coros, batch_size=batch_size, nofiles=True)
|
| 433 |
+
|
| 434 |
+
async def _put_file(self, lpath, rpath, **kwargs):
|
| 435 |
+
raise NotImplementedError
|
| 436 |
+
|
| 437 |
+
async def _put(
|
| 438 |
+
self,
|
| 439 |
+
lpath,
|
| 440 |
+
rpath,
|
| 441 |
+
recursive=False,
|
| 442 |
+
callback=_DEFAULT_CALLBACK,
|
| 443 |
+
batch_size=None,
|
| 444 |
+
**kwargs,
|
| 445 |
+
):
|
| 446 |
+
"""Copy file(s) from local.
|
| 447 |
+
|
| 448 |
+
Copies a specific file or tree of files (if recursive=True). If rpath
|
| 449 |
+
ends with a "/", it will be assumed to be a directory, and target files
|
| 450 |
+
will go within.
|
| 451 |
+
|
| 452 |
+
The put_file method will be called concurrently on a batch of files. The
|
| 453 |
+
batch_size option can configure the amount of futures that can be executed
|
| 454 |
+
at the same time. If it is -1, then all the files will be uploaded concurrently.
|
| 455 |
+
The default can be set for this instance by passing "batch_size" in the
|
| 456 |
+
constructor, or for all instances by setting the "gather_batch_size" key
|
| 457 |
+
in ``fsspec.config.conf``, falling back to 1/8th of the system limit .
|
| 458 |
+
"""
|
| 459 |
+
from .implementations.local import LocalFileSystem, make_path_posix
|
| 460 |
+
|
| 461 |
+
rpath = self._strip_protocol(rpath)
|
| 462 |
+
if isinstance(lpath, str):
|
| 463 |
+
lpath = make_path_posix(lpath)
|
| 464 |
+
fs = LocalFileSystem()
|
| 465 |
+
lpaths = fs.expand_path(lpath, recursive=recursive)
|
| 466 |
+
rpaths = other_paths(
|
| 467 |
+
lpaths, rpath, exists=isinstance(rpath, str) and await self._isdir(rpath)
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
+
is_dir = {l: os.path.isdir(l) for l in lpaths}
|
| 471 |
+
rdirs = [r for l, r in zip(lpaths, rpaths) if is_dir[l]]
|
| 472 |
+
file_pairs = [(l, r) for l, r in zip(lpaths, rpaths) if not is_dir[l]]
|
| 473 |
+
|
| 474 |
+
await asyncio.gather(*[self._makedirs(d, exist_ok=True) for d in rdirs])
|
| 475 |
+
batch_size = batch_size or self.batch_size
|
| 476 |
+
|
| 477 |
+
coros = []
|
| 478 |
+
callback.set_size(len(file_pairs))
|
| 479 |
+
for lfile, rfile in file_pairs:
|
| 480 |
+
callback.branch(lfile, rfile, kwargs)
|
| 481 |
+
coros.append(self._put_file(lfile, rfile, **kwargs))
|
| 482 |
+
|
| 483 |
+
return await _run_coros_in_chunks(
|
| 484 |
+
coros, batch_size=batch_size, callback=callback
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
async def _get_file(self, rpath, lpath, **kwargs):
|
| 488 |
+
raise NotImplementedError
|
| 489 |
+
|
| 490 |
+
async def _get(
|
| 491 |
+
self, rpath, lpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs
|
| 492 |
+
):
|
| 493 |
+
"""Copy file(s) to local.
|
| 494 |
+
|
| 495 |
+
Copies a specific file or tree of files (if recursive=True). If lpath
|
| 496 |
+
ends with a "/", it will be assumed to be a directory, and target files
|
| 497 |
+
will go within. Can submit a list of paths, which may be glob-patterns
|
| 498 |
+
and will be expanded.
|
| 499 |
+
|
| 500 |
+
The get_file method will be called concurrently on a batch of files. The
|
| 501 |
+
batch_size option can configure the amount of futures that can be executed
|
| 502 |
+
at the same time. If it is -1, then all the files will be uploaded concurrently.
|
| 503 |
+
The default can be set for this instance by passing "batch_size" in the
|
| 504 |
+
constructor, or for all instances by setting the "gather_batch_size" key
|
| 505 |
+
in ``fsspec.config.conf``, falling back to 1/8th of the system limit .
|
| 506 |
+
"""
|
| 507 |
+
from fsspec.implementations.local import make_path_posix
|
| 508 |
+
|
| 509 |
+
rpath = self._strip_protocol(rpath)
|
| 510 |
+
lpath = make_path_posix(lpath)
|
| 511 |
+
rpaths = await self._expand_path(rpath, recursive=recursive)
|
| 512 |
+
lpaths = other_paths(rpaths, lpath)
|
| 513 |
+
[os.makedirs(os.path.dirname(lp), exist_ok=True) for lp in lpaths]
|
| 514 |
+
batch_size = kwargs.pop("batch_size", self.batch_size)
|
| 515 |
+
|
| 516 |
+
coros = []
|
| 517 |
+
callback.set_size(len(lpaths))
|
| 518 |
+
for lpath, rpath in zip(lpaths, rpaths):
|
| 519 |
+
callback.branch(rpath, lpath, kwargs)
|
| 520 |
+
coros.append(self._get_file(rpath, lpath, **kwargs))
|
| 521 |
+
return await _run_coros_in_chunks(
|
| 522 |
+
coros, batch_size=batch_size, callback=callback
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
async def _isfile(self, path):
|
| 526 |
+
try:
|
| 527 |
+
return (await self._info(path))["type"] == "file"
|
| 528 |
+
except: # noqa: E722
|
| 529 |
+
return False
|
| 530 |
+
|
| 531 |
+
async def _isdir(self, path):
|
| 532 |
+
try:
|
| 533 |
+
return (await self._info(path))["type"] == "directory"
|
| 534 |
+
except IOError:
|
| 535 |
+
return False
|
| 536 |
+
|
| 537 |
+
async def _size(self, path):
|
| 538 |
+
return (await self._info(path)).get("size", None)
|
| 539 |
+
|
| 540 |
+
async def _sizes(self, paths, batch_size=None):
|
| 541 |
+
batch_size = batch_size or self.batch_size
|
| 542 |
+
return await _run_coros_in_chunks(
|
| 543 |
+
[self._size(p) for p in paths], batch_size=batch_size
|
| 544 |
+
)
|
| 545 |
+
|
| 546 |
+
async def _exists(self, path):
|
| 547 |
+
try:
|
| 548 |
+
await self._info(path)
|
| 549 |
+
return True
|
| 550 |
+
except FileNotFoundError:
|
| 551 |
+
return False
|
| 552 |
+
|
| 553 |
+
async def _info(self, path, **kwargs):
|
| 554 |
+
raise NotImplementedError
|
| 555 |
+
|
| 556 |
+
async def _ls(self, path, detail=True, **kwargs):
|
| 557 |
+
raise NotImplementedError
|
| 558 |
+
|
| 559 |
+
async def _walk(self, path, maxdepth=None, **kwargs):
|
| 560 |
+
path = self._strip_protocol(path)
|
| 561 |
+
full_dirs = {}
|
| 562 |
+
dirs = {}
|
| 563 |
+
files = {}
|
| 564 |
+
|
| 565 |
+
detail = kwargs.pop("detail", False)
|
| 566 |
+
try:
|
| 567 |
+
listing = await self._ls(path, detail=True, **kwargs)
|
| 568 |
+
except (FileNotFoundError, IOError):
|
| 569 |
+
if detail:
|
| 570 |
+
yield path, {}, {}
|
| 571 |
+
else:
|
| 572 |
+
yield path, [], []
|
| 573 |
+
return
|
| 574 |
+
|
| 575 |
+
for info in listing:
|
| 576 |
+
# each info name must be at least [path]/part , but here
|
| 577 |
+
# we check also for names like [path]/part/
|
| 578 |
+
pathname = info["name"].rstrip("/")
|
| 579 |
+
name = pathname.rsplit("/", 1)[-1]
|
| 580 |
+
if info["type"] == "directory" and pathname != path:
|
| 581 |
+
# do not include "self" path
|
| 582 |
+
full_dirs[pathname] = info
|
| 583 |
+
dirs[name] = info
|
| 584 |
+
elif pathname == path:
|
| 585 |
+
# file-like with same name as give path
|
| 586 |
+
files[""] = info
|
| 587 |
+
else:
|
| 588 |
+
files[name] = info
|
| 589 |
+
|
| 590 |
+
if detail:
|
| 591 |
+
yield path, dirs, files
|
| 592 |
+
else:
|
| 593 |
+
yield path, list(dirs), list(files)
|
| 594 |
+
|
| 595 |
+
if maxdepth is not None:
|
| 596 |
+
maxdepth -= 1
|
| 597 |
+
if maxdepth < 1:
|
| 598 |
+
return
|
| 599 |
+
|
| 600 |
+
for d in full_dirs:
|
| 601 |
+
async for _ in self._walk(d, maxdepth=maxdepth, detail=detail, **kwargs):
|
| 602 |
+
yield _
|
| 603 |
+
|
| 604 |
+
async def _glob(self, path, **kwargs):
|
| 605 |
+
import re
|
| 606 |
+
|
| 607 |
+
ends = path.endswith("/")
|
| 608 |
+
path = self._strip_protocol(path)
|
| 609 |
+
indstar = path.find("*") if path.find("*") >= 0 else len(path)
|
| 610 |
+
indques = path.find("?") if path.find("?") >= 0 else len(path)
|
| 611 |
+
indbrace = path.find("[") if path.find("[") >= 0 else len(path)
|
| 612 |
+
|
| 613 |
+
ind = min(indstar, indques, indbrace)
|
| 614 |
+
|
| 615 |
+
detail = kwargs.pop("detail", False)
|
| 616 |
+
|
| 617 |
+
if not has_magic(path):
|
| 618 |
+
root = path
|
| 619 |
+
depth = 1
|
| 620 |
+
if ends:
|
| 621 |
+
path += "/*"
|
| 622 |
+
elif await self._exists(path):
|
| 623 |
+
if not detail:
|
| 624 |
+
return [path]
|
| 625 |
+
else:
|
| 626 |
+
return {path: await self._info(path)}
|
| 627 |
+
else:
|
| 628 |
+
if not detail:
|
| 629 |
+
return [] # glob of non-existent returns empty
|
| 630 |
+
else:
|
| 631 |
+
return {}
|
| 632 |
+
elif "/" in path[:ind]:
|
| 633 |
+
ind2 = path[:ind].rindex("/")
|
| 634 |
+
root = path[: ind2 + 1]
|
| 635 |
+
depth = None if "**" in path else path[ind2 + 1 :].count("/") + 1
|
| 636 |
+
else:
|
| 637 |
+
root = ""
|
| 638 |
+
depth = None if "**" in path else path[ind + 1 :].count("/") + 1
|
| 639 |
+
|
| 640 |
+
allpaths = await self._find(
|
| 641 |
+
root, maxdepth=depth, withdirs=True, detail=True, **kwargs
|
| 642 |
+
)
|
| 643 |
+
# Escape characters special to python regex, leaving our supported
|
| 644 |
+
# special characters in place.
|
| 645 |
+
# See https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
|
| 646 |
+
# for shell globbing details.
|
| 647 |
+
pattern = (
|
| 648 |
+
"^"
|
| 649 |
+
+ (
|
| 650 |
+
path.replace("\\", r"\\")
|
| 651 |
+
.replace(".", r"\.")
|
| 652 |
+
.replace("+", r"\+")
|
| 653 |
+
.replace("//", "/")
|
| 654 |
+
.replace("(", r"\(")
|
| 655 |
+
.replace(")", r"\)")
|
| 656 |
+
.replace("|", r"\|")
|
| 657 |
+
.replace("^", r"\^")
|
| 658 |
+
.replace("$", r"\$")
|
| 659 |
+
.replace("{", r"\{")
|
| 660 |
+
.replace("}", r"\}")
|
| 661 |
+
.rstrip("/")
|
| 662 |
+
.replace("?", ".")
|
| 663 |
+
)
|
| 664 |
+
+ "$"
|
| 665 |
+
)
|
| 666 |
+
pattern = re.sub("[*]{2}", "=PLACEHOLDER=", pattern)
|
| 667 |
+
pattern = re.sub("[*]", "[^/]*", pattern)
|
| 668 |
+
pattern = re.compile(pattern.replace("=PLACEHOLDER=", ".*"))
|
| 669 |
+
out = {
|
| 670 |
+
p: allpaths[p]
|
| 671 |
+
for p in sorted(allpaths)
|
| 672 |
+
if pattern.match(p.replace("//", "/").rstrip("/"))
|
| 673 |
+
}
|
| 674 |
+
if detail:
|
| 675 |
+
return out
|
| 676 |
+
else:
|
| 677 |
+
return list(out)
|
| 678 |
+
|
| 679 |
+
async def _du(self, path, total=True, maxdepth=None, **kwargs):
|
| 680 |
+
sizes = {}
|
| 681 |
+
# async for?
|
| 682 |
+
for f in await self._find(path, maxdepth=maxdepth, **kwargs):
|
| 683 |
+
info = await self._info(f)
|
| 684 |
+
sizes[info["name"]] = info["size"]
|
| 685 |
+
if total:
|
| 686 |
+
return sum(sizes.values())
|
| 687 |
+
else:
|
| 688 |
+
return sizes
|
| 689 |
+
|
| 690 |
+
async def _find(self, path, maxdepth=None, withdirs=False, **kwargs):
|
| 691 |
+
path = self._strip_protocol(path)
|
| 692 |
+
out = dict()
|
| 693 |
+
detail = kwargs.pop("detail", False)
|
| 694 |
+
# async for?
|
| 695 |
+
async for _, dirs, files in self._walk(path, maxdepth, detail=True, **kwargs):
|
| 696 |
+
if withdirs:
|
| 697 |
+
files.update(dirs)
|
| 698 |
+
out.update({info["name"]: info for name, info in files.items()})
|
| 699 |
+
if not out and (await self._isfile(path)):
|
| 700 |
+
# walk works on directories, but find should also return [path]
|
| 701 |
+
# when path happens to be a file
|
| 702 |
+
out[path] = {}
|
| 703 |
+
names = sorted(out)
|
| 704 |
+
if not detail:
|
| 705 |
+
return names
|
| 706 |
+
else:
|
| 707 |
+
return {name: out[name] for name in names}
|
| 708 |
+
|
| 709 |
+
async def _expand_path(self, path, recursive=False, maxdepth=None):
|
| 710 |
+
if isinstance(path, str):
|
| 711 |
+
out = await self._expand_path([path], recursive, maxdepth)
|
| 712 |
+
else:
|
| 713 |
+
# reduce depth on each recursion level unless None or 0
|
| 714 |
+
maxdepth = maxdepth if not maxdepth else maxdepth - 1
|
| 715 |
+
out = set()
|
| 716 |
+
path = [self._strip_protocol(p) for p in path]
|
| 717 |
+
for p in path: # can gather here
|
| 718 |
+
if has_magic(p):
|
| 719 |
+
bit = set(await self._glob(p))
|
| 720 |
+
out |= bit
|
| 721 |
+
if recursive:
|
| 722 |
+
out |= set(
|
| 723 |
+
await self._expand_path(
|
| 724 |
+
list(bit), recursive=recursive, maxdepth=maxdepth
|
| 725 |
+
)
|
| 726 |
+
)
|
| 727 |
+
continue
|
| 728 |
+
elif recursive:
|
| 729 |
+
rec = set(await self._find(p, maxdepth=maxdepth, withdirs=True))
|
| 730 |
+
out |= rec
|
| 731 |
+
if p not in out and (recursive is False or (await self._exists(p))):
|
| 732 |
+
# should only check once, for the root
|
| 733 |
+
out.add(p)
|
| 734 |
+
if not out:
|
| 735 |
+
raise FileNotFoundError(path)
|
| 736 |
+
return list(sorted(out))
|
| 737 |
+
|
| 738 |
+
async def _mkdir(self, path, create_parents=True, **kwargs):
|
| 739 |
+
pass # not necessary to implement, may not have directories
|
| 740 |
+
|
| 741 |
+
async def _makedirs(self, path, exist_ok=False):
|
| 742 |
+
pass # not necessary to implement, may not have directories
|
| 743 |
+
|
| 744 |
+
async def open_async(self, path, mode="rb", **kwargs):
|
| 745 |
+
if "b" not in mode or kwargs.get("compression"):
|
| 746 |
+
raise ValueError
|
| 747 |
+
raise NotImplementedError
|
| 748 |
+
|
| 749 |
+
|
| 750 |
+
def mirror_sync_methods(obj):
|
| 751 |
+
"""Populate sync and async methods for obj
|
| 752 |
+
|
| 753 |
+
For each method will create a sync version if the name refers to an async method
|
| 754 |
+
(coroutine) and there is no override in the child class; will create an async
|
| 755 |
+
method for the corresponding sync method if there is no implementation.
|
| 756 |
+
|
| 757 |
+
Uses the methods specified in
|
| 758 |
+
- async_methods: the set that an implementation is expected to provide
|
| 759 |
+
- default_async_methods: that can be derived from their sync version in
|
| 760 |
+
AbstractFileSystem
|
| 761 |
+
- AsyncFileSystem: async-specific default coroutines
|
| 762 |
+
"""
|
| 763 |
+
from fsspec import AbstractFileSystem
|
| 764 |
+
|
| 765 |
+
for method in async_methods + dir(AsyncFileSystem):
|
| 766 |
+
if not method.startswith("_"):
|
| 767 |
+
continue
|
| 768 |
+
smethod = method[1:]
|
| 769 |
+
if private.match(method):
|
| 770 |
+
isco = inspect.iscoroutinefunction(getattr(obj, method, None))
|
| 771 |
+
unsync = getattr(getattr(obj, smethod, False), "__func__", None)
|
| 772 |
+
is_default = unsync is getattr(AbstractFileSystem, smethod, "")
|
| 773 |
+
if isco and is_default:
|
| 774 |
+
mth = sync_wrapper(getattr(obj, method), obj=obj)
|
| 775 |
+
setattr(obj, smethod, mth)
|
| 776 |
+
if not mth.__doc__:
|
| 777 |
+
mth.__doc__ = getattr(
|
| 778 |
+
getattr(AbstractFileSystem, smethod, None), "__doc__", ""
|
| 779 |
+
)
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
class FSSpecCoroutineCancel(Exception):
|
| 783 |
+
pass
|
| 784 |
+
|
| 785 |
+
|
| 786 |
+
def _dump_running_tasks(
|
| 787 |
+
printout=True, cancel=True, exc=FSSpecCoroutineCancel, with_task=False
|
| 788 |
+
):
|
| 789 |
+
import traceback
|
| 790 |
+
|
| 791 |
+
tasks = [t for t in asyncio.tasks.all_tasks(loop[0]) if not t.done()]
|
| 792 |
+
if printout:
|
| 793 |
+
[task.print_stack() for task in tasks]
|
| 794 |
+
out = [
|
| 795 |
+
{
|
| 796 |
+
"locals": task._coro.cr_frame.f_locals,
|
| 797 |
+
"file": task._coro.cr_frame.f_code.co_filename,
|
| 798 |
+
"firstline": task._coro.cr_frame.f_code.co_firstlineno,
|
| 799 |
+
"linelo": task._coro.cr_frame.f_lineno,
|
| 800 |
+
"stack": traceback.format_stack(task._coro.cr_frame),
|
| 801 |
+
"task": task if with_task else None,
|
| 802 |
+
}
|
| 803 |
+
for task in tasks
|
| 804 |
+
]
|
| 805 |
+
if cancel:
|
| 806 |
+
for t in tasks:
|
| 807 |
+
cbs = t._callbacks
|
| 808 |
+
t.cancel()
|
| 809 |
+
asyncio.futures.Future.set_exception(t, exc)
|
| 810 |
+
asyncio.futures.Future.cancel(t)
|
| 811 |
+
[cb[0](t) for cb in cbs] # cancels any dependent concurrent.futures
|
| 812 |
+
try:
|
| 813 |
+
t._coro.throw(exc) # exits coro, unless explicitly handled
|
| 814 |
+
except exc:
|
| 815 |
+
pass
|
| 816 |
+
return out
|
| 817 |
+
|
| 818 |
+
|
| 819 |
+
class AbstractAsyncStreamedFile(AbstractBufferedFile):
|
| 820 |
+
# no read buffering, and always auto-commit
|
| 821 |
+
# TODO: readahead might still be useful here, but needs async version
|
| 822 |
+
|
| 823 |
+
async def read(self, length=-1):
|
| 824 |
+
"""
|
| 825 |
+
Return data from cache, or fetch pieces as necessary
|
| 826 |
+
|
| 827 |
+
Parameters
|
| 828 |
+
----------
|
| 829 |
+
length: int (-1)
|
| 830 |
+
Number of bytes to read; if <0, all remaining bytes.
|
| 831 |
+
"""
|
| 832 |
+
length = -1 if length is None else int(length)
|
| 833 |
+
if self.mode != "rb":
|
| 834 |
+
raise ValueError("File not in read mode")
|
| 835 |
+
if length < 0:
|
| 836 |
+
length = self.size - self.loc
|
| 837 |
+
if self.closed:
|
| 838 |
+
raise ValueError("I/O operation on closed file.")
|
| 839 |
+
if length == 0:
|
| 840 |
+
# don't even bother calling fetch
|
| 841 |
+
return b""
|
| 842 |
+
out = await self._fetch_range(self.loc, self.loc + length)
|
| 843 |
+
self.loc += len(out)
|
| 844 |
+
return out
|
| 845 |
+
|
| 846 |
+
async def write(self, data):
|
| 847 |
+
"""
|
| 848 |
+
Write data to buffer.
|
| 849 |
+
|
| 850 |
+
Buffer only sent on flush() or if buffer is greater than
|
| 851 |
+
or equal to blocksize.
|
| 852 |
+
|
| 853 |
+
Parameters
|
| 854 |
+
----------
|
| 855 |
+
data: bytes
|
| 856 |
+
Set of bytes to be written.
|
| 857 |
+
"""
|
| 858 |
+
if self.mode not in {"wb", "ab"}:
|
| 859 |
+
raise ValueError("File not in write mode")
|
| 860 |
+
if self.closed:
|
| 861 |
+
raise ValueError("I/O operation on closed file.")
|
| 862 |
+
if self.forced:
|
| 863 |
+
raise ValueError("This file has been force-flushed, can only close")
|
| 864 |
+
out = self.buffer.write(data)
|
| 865 |
+
self.loc += out
|
| 866 |
+
if self.buffer.tell() >= self.blocksize:
|
| 867 |
+
await self.flush()
|
| 868 |
+
return out
|
| 869 |
+
|
| 870 |
+
async def close(self):
|
| 871 |
+
"""Close file
|
| 872 |
+
|
| 873 |
+
Finalizes writes, discards cache
|
| 874 |
+
"""
|
| 875 |
+
if getattr(self, "_unclosable", False):
|
| 876 |
+
return
|
| 877 |
+
if self.closed:
|
| 878 |
+
return
|
| 879 |
+
if self.mode == "rb":
|
| 880 |
+
self.cache = None
|
| 881 |
+
else:
|
| 882 |
+
if not self.forced:
|
| 883 |
+
await self.flush(force=True)
|
| 884 |
+
|
| 885 |
+
if self.fs is not None:
|
| 886 |
+
self.fs.invalidate_cache(self.path)
|
| 887 |
+
self.fs.invalidate_cache(self.fs._parent(self.path))
|
| 888 |
+
|
| 889 |
+
self.closed = True
|
| 890 |
+
|
| 891 |
+
async def flush(self, force=False):
|
| 892 |
+
if self.closed:
|
| 893 |
+
raise ValueError("Flush on closed file")
|
| 894 |
+
if force and self.forced:
|
| 895 |
+
raise ValueError("Force flush cannot be called more than once")
|
| 896 |
+
if force:
|
| 897 |
+
self.forced = True
|
| 898 |
+
|
| 899 |
+
if self.mode not in {"wb", "ab"}:
|
| 900 |
+
# no-op to flush on read-mode
|
| 901 |
+
return
|
| 902 |
+
|
| 903 |
+
if not force and self.buffer.tell() < self.blocksize:
|
| 904 |
+
# Defer write on small block
|
| 905 |
+
return
|
| 906 |
+
|
| 907 |
+
if self.offset is None:
|
| 908 |
+
# Initialize a multipart upload
|
| 909 |
+
self.offset = 0
|
| 910 |
+
try:
|
| 911 |
+
await self._initiate_upload()
|
| 912 |
+
except: # noqa: E722
|
| 913 |
+
self.closed = True
|
| 914 |
+
raise
|
| 915 |
+
|
| 916 |
+
if self._upload_chunk(final=force) is not False:
|
| 917 |
+
self.offset += self.buffer.seek(0, 2)
|
| 918 |
+
self.buffer = io.BytesIO()
|
| 919 |
+
|
| 920 |
+
async def __aenter__(self):
|
| 921 |
+
return self
|
| 922 |
+
|
| 923 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 924 |
+
await self.close()
|
| 925 |
+
|
| 926 |
+
async def _fetch_range(self, start, end):
|
| 927 |
+
raise NotImplementedError
|
| 928 |
+
|
| 929 |
+
async def _initiate_upload(self):
|
| 930 |
+
raise NotImplementedError
|
| 931 |
+
|
| 932 |
+
async def _upload_chunk(self, final=False):
|
| 933 |
+
raise NotImplementedError
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/caching.py
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import functools
|
| 2 |
+
import io
|
| 3 |
+
import logging
|
| 4 |
+
import math
|
| 5 |
+
import os
|
| 6 |
+
import warnings
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger("fsspec")
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class BaseCache(object):
|
| 12 |
+
"""Pass-though cache: doesn't keep anything, calls every time
|
| 13 |
+
|
| 14 |
+
Acts as base class for other cachers
|
| 15 |
+
|
| 16 |
+
Parameters
|
| 17 |
+
----------
|
| 18 |
+
blocksize: int
|
| 19 |
+
How far to read ahead in numbers of bytes
|
| 20 |
+
fetcher: func
|
| 21 |
+
Function of the form f(start, end) which gets bytes from remote as
|
| 22 |
+
specified
|
| 23 |
+
size: int
|
| 24 |
+
How big this file is
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
name = "none"
|
| 28 |
+
|
| 29 |
+
def __init__(self, blocksize, fetcher, size):
|
| 30 |
+
self.blocksize = blocksize
|
| 31 |
+
self.fetcher = fetcher
|
| 32 |
+
self.size = size
|
| 33 |
+
|
| 34 |
+
def _fetch(self, start, stop):
|
| 35 |
+
if start is None:
|
| 36 |
+
start = 0
|
| 37 |
+
if stop is None:
|
| 38 |
+
stop = self.size
|
| 39 |
+
if start >= self.size or start >= stop:
|
| 40 |
+
return b""
|
| 41 |
+
return self.fetcher(start, stop)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class MMapCache(BaseCache):
|
| 45 |
+
"""memory-mapped sparse file cache
|
| 46 |
+
|
| 47 |
+
Opens temporary file, which is filled blocks-wise when data is requested.
|
| 48 |
+
Ensure there is enough disc space in the temporary location.
|
| 49 |
+
|
| 50 |
+
This cache method might only work on posix
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
name = "mmap"
|
| 54 |
+
|
| 55 |
+
def __init__(self, blocksize, fetcher, size, location=None, blocks=None):
|
| 56 |
+
super().__init__(blocksize, fetcher, size)
|
| 57 |
+
self.blocks = set() if blocks is None else blocks
|
| 58 |
+
self.location = location
|
| 59 |
+
self.cache = self._makefile()
|
| 60 |
+
|
| 61 |
+
def _makefile(self):
|
| 62 |
+
import mmap
|
| 63 |
+
import tempfile
|
| 64 |
+
|
| 65 |
+
if self.size == 0:
|
| 66 |
+
return bytearray()
|
| 67 |
+
|
| 68 |
+
# posix version
|
| 69 |
+
if self.location is None or not os.path.exists(self.location):
|
| 70 |
+
if self.location is None:
|
| 71 |
+
fd = tempfile.TemporaryFile()
|
| 72 |
+
self.blocks = set()
|
| 73 |
+
else:
|
| 74 |
+
fd = io.open(self.location, "wb+")
|
| 75 |
+
fd.seek(self.size - 1)
|
| 76 |
+
fd.write(b"1")
|
| 77 |
+
fd.flush()
|
| 78 |
+
else:
|
| 79 |
+
fd = io.open(self.location, "rb+")
|
| 80 |
+
|
| 81 |
+
return mmap.mmap(fd.fileno(), self.size)
|
| 82 |
+
|
| 83 |
+
def _fetch(self, start, end):
|
| 84 |
+
logger.debug(f"MMap cache fetching {start}-{end}")
|
| 85 |
+
if start is None:
|
| 86 |
+
start = 0
|
| 87 |
+
if end is None:
|
| 88 |
+
end = self.size
|
| 89 |
+
if start >= self.size or start >= end:
|
| 90 |
+
return b""
|
| 91 |
+
start_block = start // self.blocksize
|
| 92 |
+
end_block = end // self.blocksize
|
| 93 |
+
need = [i for i in range(start_block, end_block + 1) if i not in self.blocks]
|
| 94 |
+
while need:
|
| 95 |
+
# TODO: not a for loop so we can consolidate blocks later to
|
| 96 |
+
# make fewer fetch calls; this could be parallel
|
| 97 |
+
i = need.pop(0)
|
| 98 |
+
sstart = i * self.blocksize
|
| 99 |
+
send = min(sstart + self.blocksize, self.size)
|
| 100 |
+
logger.debug(f"MMap get block #{i} ({sstart}-{send}")
|
| 101 |
+
self.cache[sstart:send] = self.fetcher(sstart, send)
|
| 102 |
+
self.blocks.add(i)
|
| 103 |
+
|
| 104 |
+
return self.cache[start:end]
|
| 105 |
+
|
| 106 |
+
def __getstate__(self):
|
| 107 |
+
state = self.__dict__.copy()
|
| 108 |
+
# Remove the unpicklable entries.
|
| 109 |
+
del state["cache"]
|
| 110 |
+
return state
|
| 111 |
+
|
| 112 |
+
def __setstate__(self, state):
|
| 113 |
+
# Restore instance attributes
|
| 114 |
+
self.__dict__.update(state)
|
| 115 |
+
self.cache = self._makefile()
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class ReadAheadCache(BaseCache):
|
| 119 |
+
"""Cache which reads only when we get beyond a block of data
|
| 120 |
+
|
| 121 |
+
This is a much simpler version of BytesCache, and does not attempt to
|
| 122 |
+
fill holes in the cache or keep fragments alive. It is best suited to
|
| 123 |
+
many small reads in a sequential order (e.g., reading lines from a file).
|
| 124 |
+
"""
|
| 125 |
+
|
| 126 |
+
name = "readahead"
|
| 127 |
+
|
| 128 |
+
def __init__(self, blocksize, fetcher, size):
|
| 129 |
+
super().__init__(blocksize, fetcher, size)
|
| 130 |
+
self.cache = b""
|
| 131 |
+
self.start = 0
|
| 132 |
+
self.end = 0
|
| 133 |
+
|
| 134 |
+
def _fetch(self, start, end):
|
| 135 |
+
if start is None:
|
| 136 |
+
start = 0
|
| 137 |
+
if end is None or end > self.size:
|
| 138 |
+
end = self.size
|
| 139 |
+
if start >= self.size or start >= end:
|
| 140 |
+
return b""
|
| 141 |
+
l = end - start
|
| 142 |
+
if start >= self.start and end <= self.end:
|
| 143 |
+
# cache hit
|
| 144 |
+
return self.cache[start - self.start : end - self.start]
|
| 145 |
+
elif self.start <= start < self.end:
|
| 146 |
+
# partial hit
|
| 147 |
+
part = self.cache[start - self.start :]
|
| 148 |
+
l -= len(part)
|
| 149 |
+
start = self.end
|
| 150 |
+
else:
|
| 151 |
+
# miss
|
| 152 |
+
part = b""
|
| 153 |
+
end = min(self.size, end + self.blocksize)
|
| 154 |
+
self.cache = self.fetcher(start, end) # new block replaces old
|
| 155 |
+
self.start = start
|
| 156 |
+
self.end = self.start + len(self.cache)
|
| 157 |
+
return part + self.cache[:l]
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class FirstChunkCache(BaseCache):
|
| 161 |
+
"""Caches the first block of a file only
|
| 162 |
+
|
| 163 |
+
This may be useful for file types where the metadata is stored in the header,
|
| 164 |
+
but is randomly accessed.
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
name = "first"
|
| 168 |
+
|
| 169 |
+
def __init__(self, blocksize, fetcher, size):
|
| 170 |
+
super().__init__(blocksize, fetcher, size)
|
| 171 |
+
self.cache = None
|
| 172 |
+
|
| 173 |
+
def _fetch(self, start, end):
|
| 174 |
+
start = start or 0
|
| 175 |
+
end = end or self.size
|
| 176 |
+
if start < self.blocksize:
|
| 177 |
+
if self.cache is None:
|
| 178 |
+
if end > self.blocksize:
|
| 179 |
+
data = self.fetcher(0, end)
|
| 180 |
+
self.cache = data[: self.blocksize]
|
| 181 |
+
return data[start:]
|
| 182 |
+
self.cache = self.fetcher(0, self.blocksize)
|
| 183 |
+
part = self.cache[start:end]
|
| 184 |
+
if end > self.blocksize:
|
| 185 |
+
part += self.fetcher(self.blocksize, end)
|
| 186 |
+
return part
|
| 187 |
+
else:
|
| 188 |
+
return self.fetcher(start, end)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
class BlockCache(BaseCache):
|
| 192 |
+
"""
|
| 193 |
+
Cache holding memory as a set of blocks.
|
| 194 |
+
|
| 195 |
+
Requests are only ever made `blocksize` at a time, and are
|
| 196 |
+
stored in an LRU cache. The least recently accessed block is
|
| 197 |
+
discarded when more than `maxblocks` are stored.
|
| 198 |
+
|
| 199 |
+
Parameters
|
| 200 |
+
----------
|
| 201 |
+
blocksize : int
|
| 202 |
+
The number of bytes to store in each block.
|
| 203 |
+
Requests are only ever made for `blocksize`, so this
|
| 204 |
+
should balance the overhead of making a request against
|
| 205 |
+
the granularity of the blocks.
|
| 206 |
+
fetcher : Callable
|
| 207 |
+
size : int
|
| 208 |
+
The total size of the file being cached.
|
| 209 |
+
maxblocks : int
|
| 210 |
+
The maximum number of blocks to cache for. The maximum memory
|
| 211 |
+
use for this cache is then ``blocksize * maxblocks``.
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
name = "blockcache"
|
| 215 |
+
|
| 216 |
+
def __init__(self, blocksize, fetcher, size, maxblocks=32):
|
| 217 |
+
super().__init__(blocksize, fetcher, size)
|
| 218 |
+
self.nblocks = math.ceil(size / blocksize)
|
| 219 |
+
self.maxblocks = maxblocks
|
| 220 |
+
self._fetch_block_cached = functools.lru_cache(maxblocks)(self._fetch_block)
|
| 221 |
+
|
| 222 |
+
def __repr__(self):
|
| 223 |
+
return "<BlockCache blocksize={}, size={}, nblocks={}>".format(
|
| 224 |
+
self.blocksize, self.size, self.nblocks
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
def cache_info(self):
|
| 228 |
+
"""
|
| 229 |
+
The statistics on the block cache.
|
| 230 |
+
|
| 231 |
+
Returns
|
| 232 |
+
-------
|
| 233 |
+
NamedTuple
|
| 234 |
+
Returned directly from the LRU Cache used internally.
|
| 235 |
+
"""
|
| 236 |
+
return self._fetch_block_cached.cache_info()
|
| 237 |
+
|
| 238 |
+
def __getstate__(self):
|
| 239 |
+
state = self.__dict__
|
| 240 |
+
del state["_fetch_block_cached"]
|
| 241 |
+
return state
|
| 242 |
+
|
| 243 |
+
def __setstate__(self, state):
|
| 244 |
+
self.__dict__.update(state)
|
| 245 |
+
self._fetch_block_cached = functools.lru_cache(state["maxblocks"])(
|
| 246 |
+
self._fetch_block
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
def _fetch(self, start, end):
|
| 250 |
+
if start is None:
|
| 251 |
+
start = 0
|
| 252 |
+
if end is None:
|
| 253 |
+
end = self.size
|
| 254 |
+
if start >= self.size or start >= end:
|
| 255 |
+
return b""
|
| 256 |
+
|
| 257 |
+
# byte position -> block numbers
|
| 258 |
+
start_block_number = start // self.blocksize
|
| 259 |
+
end_block_number = end // self.blocksize
|
| 260 |
+
|
| 261 |
+
# these are cached, so safe to do multiple calls for the same start and end.
|
| 262 |
+
for block_number in range(start_block_number, end_block_number + 1):
|
| 263 |
+
self._fetch_block_cached(block_number)
|
| 264 |
+
|
| 265 |
+
return self._read_cache(
|
| 266 |
+
start,
|
| 267 |
+
end,
|
| 268 |
+
start_block_number=start_block_number,
|
| 269 |
+
end_block_number=end_block_number,
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
def _fetch_block(self, block_number):
|
| 273 |
+
"""
|
| 274 |
+
Fetch the block of data for `block_number`.
|
| 275 |
+
"""
|
| 276 |
+
if block_number > self.nblocks:
|
| 277 |
+
raise ValueError(
|
| 278 |
+
"'block_number={}' is greater than the number of blocks ({})".format(
|
| 279 |
+
block_number, self.nblocks
|
| 280 |
+
)
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
start = block_number * self.blocksize
|
| 284 |
+
end = start + self.blocksize
|
| 285 |
+
logger.info("BlockCache fetching block %d", block_number)
|
| 286 |
+
block_contents = super()._fetch(start, end)
|
| 287 |
+
return block_contents
|
| 288 |
+
|
| 289 |
+
def _read_cache(self, start, end, start_block_number, end_block_number):
|
| 290 |
+
"""
|
| 291 |
+
Read from our block cache.
|
| 292 |
+
|
| 293 |
+
Parameters
|
| 294 |
+
----------
|
| 295 |
+
start, end : int
|
| 296 |
+
The start and end byte positions.
|
| 297 |
+
start_block_number, end_block_number : int
|
| 298 |
+
The start and end block numbers.
|
| 299 |
+
"""
|
| 300 |
+
start_pos = start % self.blocksize
|
| 301 |
+
end_pos = end % self.blocksize
|
| 302 |
+
|
| 303 |
+
if start_block_number == end_block_number:
|
| 304 |
+
block = self._fetch_block_cached(start_block_number)
|
| 305 |
+
return block[start_pos:end_pos]
|
| 306 |
+
|
| 307 |
+
else:
|
| 308 |
+
# read from the initial
|
| 309 |
+
out = []
|
| 310 |
+
out.append(self._fetch_block_cached(start_block_number)[start_pos:])
|
| 311 |
+
|
| 312 |
+
# intermediate blocks
|
| 313 |
+
# Note: it'd be nice to combine these into one big request. However
|
| 314 |
+
# that doesn't play nicely with our LRU cache.
|
| 315 |
+
for block_number in range(start_block_number + 1, end_block_number):
|
| 316 |
+
out.append(self._fetch_block_cached(block_number))
|
| 317 |
+
|
| 318 |
+
# final block
|
| 319 |
+
out.append(self._fetch_block_cached(end_block_number)[:end_pos])
|
| 320 |
+
|
| 321 |
+
return b"".join(out)
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class BytesCache(BaseCache):
|
| 325 |
+
"""Cache which holds data in a in-memory bytes object
|
| 326 |
+
|
| 327 |
+
Implements read-ahead by the block size, for semi-random reads progressing
|
| 328 |
+
through the file.
|
| 329 |
+
|
| 330 |
+
Parameters
|
| 331 |
+
----------
|
| 332 |
+
trim: bool
|
| 333 |
+
As we read more data, whether to discard the start of the buffer when
|
| 334 |
+
we are more than a blocksize ahead of it.
|
| 335 |
+
"""
|
| 336 |
+
|
| 337 |
+
name = "bytes"
|
| 338 |
+
|
| 339 |
+
def __init__(self, blocksize, fetcher, size, trim=True):
|
| 340 |
+
super().__init__(blocksize, fetcher, size)
|
| 341 |
+
self.cache = b""
|
| 342 |
+
self.start = None
|
| 343 |
+
self.end = None
|
| 344 |
+
self.trim = trim
|
| 345 |
+
|
| 346 |
+
def _fetch(self, start, end):
|
| 347 |
+
# TODO: only set start/end after fetch, in case it fails?
|
| 348 |
+
# is this where retry logic might go?
|
| 349 |
+
if start is None:
|
| 350 |
+
start = 0
|
| 351 |
+
if end is None:
|
| 352 |
+
end = self.size
|
| 353 |
+
if start >= self.size or start >= end:
|
| 354 |
+
return b""
|
| 355 |
+
if (
|
| 356 |
+
self.start is not None
|
| 357 |
+
and start >= self.start
|
| 358 |
+
and self.end is not None
|
| 359 |
+
and end < self.end
|
| 360 |
+
):
|
| 361 |
+
# cache hit: we have all the required data
|
| 362 |
+
offset = start - self.start
|
| 363 |
+
return self.cache[offset : offset + end - start]
|
| 364 |
+
|
| 365 |
+
if self.blocksize:
|
| 366 |
+
bend = min(self.size, end + self.blocksize)
|
| 367 |
+
else:
|
| 368 |
+
bend = end
|
| 369 |
+
|
| 370 |
+
if bend == start or start > self.size:
|
| 371 |
+
return b""
|
| 372 |
+
|
| 373 |
+
if (self.start is None or start < self.start) and (
|
| 374 |
+
self.end is None or end > self.end
|
| 375 |
+
):
|
| 376 |
+
# First read, or extending both before and after
|
| 377 |
+
self.cache = self.fetcher(start, bend)
|
| 378 |
+
self.start = start
|
| 379 |
+
elif start < self.start:
|
| 380 |
+
if self.end - end > self.blocksize:
|
| 381 |
+
self.cache = self.fetcher(start, bend)
|
| 382 |
+
self.start = start
|
| 383 |
+
else:
|
| 384 |
+
new = self.fetcher(start, self.start)
|
| 385 |
+
self.start = start
|
| 386 |
+
self.cache = new + self.cache
|
| 387 |
+
elif bend > self.end:
|
| 388 |
+
if self.end > self.size:
|
| 389 |
+
pass
|
| 390 |
+
elif end - self.end > self.blocksize:
|
| 391 |
+
self.cache = self.fetcher(start, bend)
|
| 392 |
+
self.start = start
|
| 393 |
+
else:
|
| 394 |
+
new = self.fetcher(self.end, bend)
|
| 395 |
+
self.cache = self.cache + new
|
| 396 |
+
|
| 397 |
+
self.end = self.start + len(self.cache)
|
| 398 |
+
offset = start - self.start
|
| 399 |
+
out = self.cache[offset : offset + end - start]
|
| 400 |
+
if self.trim:
|
| 401 |
+
num = (self.end - self.start) // (self.blocksize + 1)
|
| 402 |
+
if num > 1:
|
| 403 |
+
self.start += self.blocksize * num
|
| 404 |
+
self.cache = self.cache[self.blocksize * num :]
|
| 405 |
+
return out
|
| 406 |
+
|
| 407 |
+
def __len__(self):
|
| 408 |
+
return len(self.cache)
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
class AllBytes(BaseCache):
|
| 412 |
+
"""Cache entire contents of the file"""
|
| 413 |
+
|
| 414 |
+
name = "all"
|
| 415 |
+
|
| 416 |
+
def __init__(self, blocksize=None, fetcher=None, size=None, data=None):
|
| 417 |
+
super().__init__(blocksize, fetcher, size)
|
| 418 |
+
if data is None:
|
| 419 |
+
data = self.fetcher(0, self.size)
|
| 420 |
+
self.data = data
|
| 421 |
+
|
| 422 |
+
def _fetch(self, start, end):
|
| 423 |
+
return self.data[start:end]
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class KnownPartsOfAFile(BaseCache):
|
| 427 |
+
"""
|
| 428 |
+
Cache holding known file parts.
|
| 429 |
+
|
| 430 |
+
Parameters
|
| 431 |
+
----------
|
| 432 |
+
blocksize: int
|
| 433 |
+
How far to read ahead in numbers of bytes
|
| 434 |
+
fetcher: func
|
| 435 |
+
Function of the form f(start, end) which gets bytes from remote as
|
| 436 |
+
specified
|
| 437 |
+
size: int
|
| 438 |
+
How big this file is
|
| 439 |
+
data: dict
|
| 440 |
+
A dictionary mapping explicit `(start, stop)` file-offset tuples
|
| 441 |
+
with known bytes.
|
| 442 |
+
strict: bool, default True
|
| 443 |
+
Whether to fetch reads that go beyond a known byte-range boundary.
|
| 444 |
+
If `False`, any read that ends outside a known part will be zero
|
| 445 |
+
padded. Note that zero padding will not be used for reads that
|
| 446 |
+
begin outside a known byte-range.
|
| 447 |
+
"""
|
| 448 |
+
|
| 449 |
+
name = "parts"
|
| 450 |
+
|
| 451 |
+
def __init__(self, blocksize, fetcher, size, data={}, strict=True, **_):
|
| 452 |
+
super(KnownPartsOfAFile, self).__init__(blocksize, fetcher, size)
|
| 453 |
+
self.strict = strict
|
| 454 |
+
|
| 455 |
+
# simple consolidation of contiguous blocks
|
| 456 |
+
if data:
|
| 457 |
+
old_offsets = sorted(list(data.keys()))
|
| 458 |
+
offsets = [old_offsets[0]]
|
| 459 |
+
blocks = [data.pop(old_offsets[0])]
|
| 460 |
+
for start, stop in old_offsets[1:]:
|
| 461 |
+
start0, stop0 = offsets[-1]
|
| 462 |
+
if start == stop0:
|
| 463 |
+
offsets[-1] = (start0, stop)
|
| 464 |
+
blocks[-1] += data.pop((start, stop))
|
| 465 |
+
else:
|
| 466 |
+
offsets.append((start, stop))
|
| 467 |
+
blocks.append(data.pop((start, stop)))
|
| 468 |
+
|
| 469 |
+
self.data = dict(zip(offsets, blocks))
|
| 470 |
+
else:
|
| 471 |
+
self.data = data
|
| 472 |
+
|
| 473 |
+
def _fetch(self, start, stop):
|
| 474 |
+
out = b""
|
| 475 |
+
for (loc0, loc1), data in self.data.items():
|
| 476 |
+
# If self.strict=False, use zero-padded data
|
| 477 |
+
# for reads beyond the end of a "known" buffer
|
| 478 |
+
if loc0 <= start < loc1:
|
| 479 |
+
off = start - loc0
|
| 480 |
+
out = data[off : off + stop - start]
|
| 481 |
+
if not self.strict or loc0 <= stop <= loc1:
|
| 482 |
+
# The request is within a known range, or
|
| 483 |
+
# it begins within a known range, and we
|
| 484 |
+
# are allowed to pad reads beyond the
|
| 485 |
+
# buffer with zero
|
| 486 |
+
out += b"\x00" * (stop - start - len(out))
|
| 487 |
+
return out
|
| 488 |
+
else:
|
| 489 |
+
# The request ends outside a known range,
|
| 490 |
+
# and we are being "strict" about reads
|
| 491 |
+
# beyond the buffer
|
| 492 |
+
start = loc1
|
| 493 |
+
break
|
| 494 |
+
|
| 495 |
+
# We only get here if there is a request outside the
|
| 496 |
+
# known parts of the file. In an ideal world, this
|
| 497 |
+
# should never happen
|
| 498 |
+
if self.fetcher is None:
|
| 499 |
+
# We cannot fetch the data, so raise an error
|
| 500 |
+
raise ValueError(f"Read is outside the known file parts: {(start, stop)}. ")
|
| 501 |
+
# We can fetch the data, but should warn the user
|
| 502 |
+
# that this may be slow
|
| 503 |
+
warnings.warn(
|
| 504 |
+
f"Read is outside the known file parts: {(start, stop)}. "
|
| 505 |
+
f"IO/caching performance may be poor!"
|
| 506 |
+
)
|
| 507 |
+
logger.debug(f"KnownPartsOfAFile cache fetching {start}-{stop}")
|
| 508 |
+
return out + super()._fetch(start, stop)
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
caches = {
|
| 512 |
+
"none": BaseCache,
|
| 513 |
+
None: BaseCache,
|
| 514 |
+
"mmap": MMapCache,
|
| 515 |
+
"bytes": BytesCache,
|
| 516 |
+
"readahead": ReadAheadCache,
|
| 517 |
+
"block": BlockCache,
|
| 518 |
+
"first": FirstChunkCache,
|
| 519 |
+
"all": AllBytes,
|
| 520 |
+
"parts": KnownPartsOfAFile,
|
| 521 |
+
}
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/callbacks.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class Callback:
|
| 2 |
+
"""
|
| 3 |
+
Base class and interface for callback mechanism
|
| 4 |
+
|
| 5 |
+
This class can be used directly for monitoring file transfers by
|
| 6 |
+
providing ``callback=Callback(hooks=...)`` (see the ``hooks`` argument,
|
| 7 |
+
below), or subclassed for more specialised behaviour.
|
| 8 |
+
|
| 9 |
+
Parameters
|
| 10 |
+
----------
|
| 11 |
+
size: int (optional)
|
| 12 |
+
Nominal quantity for the value that corresponds to a complete
|
| 13 |
+
transfer, e.g., total number of tiles or total number of
|
| 14 |
+
bytes
|
| 15 |
+
value: int (0)
|
| 16 |
+
Starting internal counter value
|
| 17 |
+
hooks: dict or None
|
| 18 |
+
A dict of named functions to be called on each update. The signature
|
| 19 |
+
of these must be ``f(size, value, **kwargs)``
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def __init__(self, size=None, value=0, hooks=None, **kwargs):
|
| 23 |
+
self.size = size
|
| 24 |
+
self.value = value
|
| 25 |
+
self.hooks = hooks or {}
|
| 26 |
+
self.kw = kwargs
|
| 27 |
+
|
| 28 |
+
def set_size(self, size):
|
| 29 |
+
"""
|
| 30 |
+
Set the internal maximum size attribute
|
| 31 |
+
|
| 32 |
+
Usually called if not initially set at instantiation. Note that this
|
| 33 |
+
triggers a ``call()``.
|
| 34 |
+
|
| 35 |
+
Parameters
|
| 36 |
+
----------
|
| 37 |
+
size: int
|
| 38 |
+
"""
|
| 39 |
+
self.size = size
|
| 40 |
+
self.call()
|
| 41 |
+
|
| 42 |
+
def absolute_update(self, value):
|
| 43 |
+
"""
|
| 44 |
+
Set the internal value state
|
| 45 |
+
|
| 46 |
+
Triggers ``call()``
|
| 47 |
+
|
| 48 |
+
Parameters
|
| 49 |
+
----------
|
| 50 |
+
value: int
|
| 51 |
+
"""
|
| 52 |
+
self.value = value
|
| 53 |
+
self.call()
|
| 54 |
+
|
| 55 |
+
def relative_update(self, inc=1):
|
| 56 |
+
"""
|
| 57 |
+
Delta increment the internal counter
|
| 58 |
+
|
| 59 |
+
Triggers ``call()``
|
| 60 |
+
|
| 61 |
+
Parameters
|
| 62 |
+
----------
|
| 63 |
+
inc: int
|
| 64 |
+
"""
|
| 65 |
+
self.value += inc
|
| 66 |
+
self.call()
|
| 67 |
+
|
| 68 |
+
def call(self, hook_name=None, **kwargs):
|
| 69 |
+
"""
|
| 70 |
+
Execute hook(s) with current state
|
| 71 |
+
|
| 72 |
+
Each function is passed the internal size and current value
|
| 73 |
+
|
| 74 |
+
Parameters
|
| 75 |
+
----------
|
| 76 |
+
hook_name: str or None
|
| 77 |
+
If given, execute on this hook
|
| 78 |
+
kwargs: passed on to (all) hook(s)
|
| 79 |
+
"""
|
| 80 |
+
if not self.hooks:
|
| 81 |
+
return
|
| 82 |
+
kw = self.kw.copy()
|
| 83 |
+
kw.update(kwargs)
|
| 84 |
+
if hook_name:
|
| 85 |
+
if hook_name not in self.hooks:
|
| 86 |
+
return
|
| 87 |
+
return self.hooks[hook_name](self.size, self.value, **kw)
|
| 88 |
+
for hook in self.hooks.values() or []:
|
| 89 |
+
hook(self.size, self.value, **kw)
|
| 90 |
+
|
| 91 |
+
def wrap(self, iterable):
|
| 92 |
+
"""
|
| 93 |
+
Wrap an iterable to call ``relative_update`` on each iterations
|
| 94 |
+
|
| 95 |
+
Parameters
|
| 96 |
+
----------
|
| 97 |
+
iterable: Iterable
|
| 98 |
+
The iterable that is being wrapped
|
| 99 |
+
"""
|
| 100 |
+
for item in iterable:
|
| 101 |
+
self.relative_update()
|
| 102 |
+
yield item
|
| 103 |
+
|
| 104 |
+
def branch(self, path_1, path_2, kwargs):
|
| 105 |
+
"""
|
| 106 |
+
Set callbacks for child transfers
|
| 107 |
+
|
| 108 |
+
If this callback is operating at a higher level, e.g., put, which may
|
| 109 |
+
trigger transfers that can also be monitored. The passed kwargs are
|
| 110 |
+
to be *mutated* to add ``callback=``, if this class supports branching
|
| 111 |
+
to children.
|
| 112 |
+
|
| 113 |
+
Parameters
|
| 114 |
+
----------
|
| 115 |
+
path_1: str
|
| 116 |
+
Child's source path
|
| 117 |
+
path_2: str
|
| 118 |
+
Child's destination path
|
| 119 |
+
kwargs: dict
|
| 120 |
+
arguments passed to child method, e.g., put_file.
|
| 121 |
+
|
| 122 |
+
Returns
|
| 123 |
+
-------
|
| 124 |
+
|
| 125 |
+
"""
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
def no_op(self, *_, **__):
|
| 129 |
+
pass
|
| 130 |
+
|
| 131 |
+
def __getattr__(self, item):
|
| 132 |
+
"""
|
| 133 |
+
If undefined methods are called on this class, nothing happens
|
| 134 |
+
"""
|
| 135 |
+
return self.no_op
|
| 136 |
+
|
| 137 |
+
@classmethod
|
| 138 |
+
def as_callback(cls, maybe_callback=None):
|
| 139 |
+
"""Transform callback=... into Callback instance
|
| 140 |
+
|
| 141 |
+
For the special value of ``None``, return the global instance of
|
| 142 |
+
``NoOpCallback``. This is an alternative to including
|
| 143 |
+
``callback=_DEFAULT_CALLBACK`` directly in a method signature.
|
| 144 |
+
"""
|
| 145 |
+
if maybe_callback is None:
|
| 146 |
+
return _DEFAULT_CALLBACK
|
| 147 |
+
return maybe_callback
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class NoOpCallback(Callback):
|
| 151 |
+
"""
|
| 152 |
+
This implementation of Callback does exactly nothing
|
| 153 |
+
"""
|
| 154 |
+
|
| 155 |
+
def call(self, *args, **kwargs):
|
| 156 |
+
return None
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class DotPrinterCallback(Callback):
|
| 160 |
+
"""
|
| 161 |
+
Simple example Callback implementation
|
| 162 |
+
|
| 163 |
+
Almost identical to Callback with a hook that prints a char; here we
|
| 164 |
+
demonstrate how the outer layer may print "#" and the inner layer "."
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
def __init__(self, chr_to_print="#", **kwargs):
|
| 168 |
+
self.chr = chr_to_print
|
| 169 |
+
super().__init__(**kwargs)
|
| 170 |
+
|
| 171 |
+
def branch(self, path_1, path_2, kwargs):
|
| 172 |
+
"""Mutate kwargs to add new instance with different print char"""
|
| 173 |
+
kwargs["callback"] = DotPrinterCallback(".")
|
| 174 |
+
|
| 175 |
+
def call(self, **kwargs):
|
| 176 |
+
"""Just outputs a character"""
|
| 177 |
+
print(self.chr, end="")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class TqdmCallback(Callback):
|
| 181 |
+
"""
|
| 182 |
+
A callback to display a progress bar using tqdm
|
| 183 |
+
|
| 184 |
+
Parameters
|
| 185 |
+
----------
|
| 186 |
+
tqdm_kwargs : dict, (optional)
|
| 187 |
+
Any argument accepted by the tqdm constructor.
|
| 188 |
+
See the `tqdm doc <https://tqdm.github.io/docs/tqdm/#__init__>`_.
|
| 189 |
+
Will be forwarded to tqdm.
|
| 190 |
+
|
| 191 |
+
Examples
|
| 192 |
+
--------
|
| 193 |
+
>>> import fsspec
|
| 194 |
+
>>> from fsspec.callbacks import TqdmCallback
|
| 195 |
+
>>> fs = fsspec.filesystem("memory")
|
| 196 |
+
>>> path2distant_data = "/your-path"
|
| 197 |
+
>>> fs.upload(
|
| 198 |
+
".",
|
| 199 |
+
path2distant_data,
|
| 200 |
+
recursive=True,
|
| 201 |
+
callback=TqdmCallback(),
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
You can forward args to tqdm using the `tqdm_kwargs` parameter.
|
| 205 |
+
|
| 206 |
+
>>> fs.upload(
|
| 207 |
+
".",
|
| 208 |
+
path2distant_data,
|
| 209 |
+
recursive=True,
|
| 210 |
+
callback=TqdmCallback(tqdm_kwargs={"desc": "Your tqdm description"}),
|
| 211 |
+
)
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
def __init__(self, tqdm_kwargs=None, *args, **kwargs):
|
| 215 |
+
try:
|
| 216 |
+
import tqdm
|
| 217 |
+
|
| 218 |
+
self._tqdm = tqdm
|
| 219 |
+
except ImportError as exce:
|
| 220 |
+
raise ImportError(
|
| 221 |
+
"Using TqdmCallback requires tqdm to be installed"
|
| 222 |
+
) from exce
|
| 223 |
+
|
| 224 |
+
self._tqdm_kwargs = tqdm_kwargs or {}
|
| 225 |
+
super().__init__(*args, **kwargs)
|
| 226 |
+
|
| 227 |
+
def set_size(self, size):
|
| 228 |
+
self.tqdm = self._tqdm.tqdm(total=size, **self._tqdm_kwargs)
|
| 229 |
+
|
| 230 |
+
def relative_update(self, inc=1):
|
| 231 |
+
self.tqdm.update(inc)
|
| 232 |
+
|
| 233 |
+
def __del__(self):
|
| 234 |
+
self.tqdm.close()
|
| 235 |
+
self.tqdm = None
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
_DEFAULT_CALLBACK = NoOpCallback()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/compression.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helper functions for a standard streaming compression API"""
|
| 2 |
+
from bz2 import BZ2File
|
| 3 |
+
from zipfile import ZipFile
|
| 4 |
+
|
| 5 |
+
import fsspec.utils
|
| 6 |
+
from fsspec.spec import AbstractBufferedFile
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def noop_file(file, mode, **kwargs):
|
| 10 |
+
return file
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# TODO: files should also be available as contexts
|
| 14 |
+
# should be functions of the form func(infile, mode=, **kwargs) -> file-like
|
| 15 |
+
compr = {None: noop_file}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def register_compression(name, callback, extensions, force=False):
|
| 19 |
+
"""Register an "inferable" file compression type.
|
| 20 |
+
|
| 21 |
+
Registers transparent file compression type for use with fsspec.open.
|
| 22 |
+
Compression can be specified by name in open, or "infer"-ed for any files
|
| 23 |
+
ending with the given extensions.
|
| 24 |
+
|
| 25 |
+
Args:
|
| 26 |
+
name: (str) The compression type name. Eg. "gzip".
|
| 27 |
+
callback: A callable of form (infile, mode, **kwargs) -> file-like.
|
| 28 |
+
Accepts an input file-like object, the target mode and kwargs.
|
| 29 |
+
Returns a wrapped file-like object.
|
| 30 |
+
extensions: (str, Iterable[str]) A file extension, or list of file
|
| 31 |
+
extensions for which to infer this compression scheme. Eg. "gz".
|
| 32 |
+
force: (bool) Force re-registration of compression type or extensions.
|
| 33 |
+
|
| 34 |
+
Raises:
|
| 35 |
+
ValueError: If name or extensions already registered, and not force.
|
| 36 |
+
|
| 37 |
+
"""
|
| 38 |
+
if isinstance(extensions, str):
|
| 39 |
+
extensions = [extensions]
|
| 40 |
+
|
| 41 |
+
# Validate registration
|
| 42 |
+
if name in compr and not force:
|
| 43 |
+
raise ValueError("Duplicate compression registration: %s" % name)
|
| 44 |
+
|
| 45 |
+
for ext in extensions:
|
| 46 |
+
if ext in fsspec.utils.compressions and not force:
|
| 47 |
+
raise ValueError(
|
| 48 |
+
"Duplicate compression file extension: %s (%s)" % (ext, name)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
compr[name] = callback
|
| 52 |
+
|
| 53 |
+
for ext in extensions:
|
| 54 |
+
fsspec.utils.compressions[ext] = name
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def unzip(infile, mode="rb", filename=None, **kwargs):
|
| 58 |
+
if "r" not in mode:
|
| 59 |
+
filename = filename or "file"
|
| 60 |
+
z = ZipFile(infile, mode="w", **kwargs)
|
| 61 |
+
fo = z.open(filename, mode="w")
|
| 62 |
+
fo.close = lambda closer=fo.close: closer() or z.close()
|
| 63 |
+
return fo
|
| 64 |
+
z = ZipFile(infile)
|
| 65 |
+
if filename is None:
|
| 66 |
+
filename = z.namelist()[0]
|
| 67 |
+
return z.open(filename, mode="r", **kwargs)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
register_compression("zip", unzip, "zip")
|
| 71 |
+
register_compression("bz2", BZ2File, "bz2")
|
| 72 |
+
|
| 73 |
+
try: # pragma: no cover
|
| 74 |
+
from isal import igzip
|
| 75 |
+
|
| 76 |
+
# igzip is meant to be used as a faster drop in replacement to gzip
|
| 77 |
+
# so its api and functions are the same as the stdlib’s module. Except
|
| 78 |
+
# where ISA-L does not support the same calls as zlib
|
| 79 |
+
# (See https://python-isal.readthedocs.io/).
|
| 80 |
+
|
| 81 |
+
register_compression("gzip", igzip.IGzipFile, "gz")
|
| 82 |
+
except ImportError:
|
| 83 |
+
from gzip import GzipFile
|
| 84 |
+
|
| 85 |
+
register_compression(
|
| 86 |
+
"gzip", lambda f, **kwargs: GzipFile(fileobj=f, **kwargs), "gz"
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
from lzma import LZMAFile
|
| 91 |
+
|
| 92 |
+
register_compression("lzma", LZMAFile, "xz")
|
| 93 |
+
register_compression("xz", LZMAFile, "xz", force=True)
|
| 94 |
+
except ImportError:
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
import lzmaffi
|
| 99 |
+
|
| 100 |
+
register_compression("lzma", lzmaffi.LZMAFile, "xz", force=True)
|
| 101 |
+
register_compression("xz", lzmaffi.LZMAFile, "xz", force=True)
|
| 102 |
+
except ImportError:
|
| 103 |
+
pass
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class SnappyFile(AbstractBufferedFile):
|
| 107 |
+
def __init__(self, infile, mode, **kwargs):
|
| 108 |
+
import snappy
|
| 109 |
+
|
| 110 |
+
super().__init__(
|
| 111 |
+
fs=None, path="snappy", mode=mode.strip("b") + "b", size=999999999, **kwargs
|
| 112 |
+
)
|
| 113 |
+
self.infile = infile
|
| 114 |
+
if "r" in mode:
|
| 115 |
+
self.codec = snappy.StreamDecompressor()
|
| 116 |
+
else:
|
| 117 |
+
self.codec = snappy.StreamCompressor()
|
| 118 |
+
|
| 119 |
+
def _upload_chunk(self, final=False):
|
| 120 |
+
self.buffer.seek(0)
|
| 121 |
+
out = self.codec.add_chunk(self.buffer.read())
|
| 122 |
+
self.infile.write(out)
|
| 123 |
+
return True
|
| 124 |
+
|
| 125 |
+
def seek(self, loc, whence=0):
|
| 126 |
+
raise NotImplementedError("SnappyFile is not seekable")
|
| 127 |
+
|
| 128 |
+
def seekable(self):
|
| 129 |
+
return False
|
| 130 |
+
|
| 131 |
+
def _fetch_range(self, start, end):
|
| 132 |
+
"""Get the specified set of bytes from remote"""
|
| 133 |
+
data = self.infile.read(end - start)
|
| 134 |
+
return self.codec.decompress(data)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
try:
|
| 138 |
+
import snappy
|
| 139 |
+
|
| 140 |
+
snappy.compress
|
| 141 |
+
# Snappy may use the .sz file extension, but this is not part of the
|
| 142 |
+
# standard implementation.
|
| 143 |
+
register_compression("snappy", SnappyFile, [])
|
| 144 |
+
|
| 145 |
+
except (ImportError, NameError):
|
| 146 |
+
pass
|
| 147 |
+
|
| 148 |
+
try:
|
| 149 |
+
import lz4.frame
|
| 150 |
+
|
| 151 |
+
register_compression("lz4", lz4.frame.open, "lz4")
|
| 152 |
+
except ImportError:
|
| 153 |
+
pass
|
| 154 |
+
|
| 155 |
+
try:
|
| 156 |
+
import zstandard as zstd
|
| 157 |
+
|
| 158 |
+
def zstandard_file(infile, mode="rb"):
|
| 159 |
+
if "r" in mode:
|
| 160 |
+
cctx = zstd.ZstdDecompressor()
|
| 161 |
+
return cctx.stream_reader(infile)
|
| 162 |
+
else:
|
| 163 |
+
cctx = zstd.ZstdCompressor(level=10)
|
| 164 |
+
return cctx.stream_writer(infile)
|
| 165 |
+
|
| 166 |
+
register_compression("zstd", zstandard_file, "zst")
|
| 167 |
+
except ImportError:
|
| 168 |
+
pass
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def available_compressions():
|
| 172 |
+
"""Return a list of the implemented compressions."""
|
| 173 |
+
return list(compr)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/config.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import configparser
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
conf = {}
|
| 6 |
+
default_conf_dir = os.path.join(os.path.expanduser("~"), ".config/fsspec")
|
| 7 |
+
conf_dir = os.environ.get("FSSPEC_CONFIG_DIR", default_conf_dir)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def set_conf_env(conf_dict, envdict=os.environ):
|
| 11 |
+
"""Set config values from environment variables
|
| 12 |
+
|
| 13 |
+
Looks for variable of the form ``FSSPEC_<protocol>_<kwarg>``.
|
| 14 |
+
There is no attempt to convert strings, but the kwarg keys will
|
| 15 |
+
be lower-cased.
|
| 16 |
+
|
| 17 |
+
Parameters
|
| 18 |
+
----------
|
| 19 |
+
conf_dict : dict(str, dict)
|
| 20 |
+
This dict will be mutated
|
| 21 |
+
envdict : dict-like(str, str)
|
| 22 |
+
Source for the values - usually the real environment
|
| 23 |
+
"""
|
| 24 |
+
for key in envdict:
|
| 25 |
+
if key.startswith("FSSPEC"):
|
| 26 |
+
if key.count("_") < 2:
|
| 27 |
+
continue
|
| 28 |
+
_, proto, kwarg = key.split("_", 2)
|
| 29 |
+
conf_dict.setdefault(proto.lower(), {})[kwarg.lower()] = envdict[key]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def set_conf_files(cdir, conf_dict):
|
| 33 |
+
"""Set config values from files
|
| 34 |
+
|
| 35 |
+
Scans for INI and JSON files in the given dictionary, and uses their
|
| 36 |
+
contents to set the config. In case of repeated values, later values
|
| 37 |
+
win.
|
| 38 |
+
|
| 39 |
+
In the case of INI files, all values are strings, and these will not
|
| 40 |
+
be converted.
|
| 41 |
+
|
| 42 |
+
Parameters
|
| 43 |
+
----------
|
| 44 |
+
cdir : str
|
| 45 |
+
Directory to search
|
| 46 |
+
conf_dict : dict(str, dict)
|
| 47 |
+
This dict will be mutated
|
| 48 |
+
"""
|
| 49 |
+
if not os.path.isdir(cdir):
|
| 50 |
+
return
|
| 51 |
+
allfiles = sorted(os.listdir(cdir))
|
| 52 |
+
for fn in allfiles:
|
| 53 |
+
if fn.endswith(".ini"):
|
| 54 |
+
ini = configparser.ConfigParser()
|
| 55 |
+
ini.read(os.path.join(cdir, fn))
|
| 56 |
+
for key in ini:
|
| 57 |
+
if key == "DEFAULT":
|
| 58 |
+
continue
|
| 59 |
+
conf_dict.setdefault(key, {}).update(dict(ini[key]))
|
| 60 |
+
if fn.endswith(".json"):
|
| 61 |
+
with open(os.path.join(cdir, fn)) as f:
|
| 62 |
+
js = json.load(f)
|
| 63 |
+
for key in js:
|
| 64 |
+
conf_dict.setdefault(key, {}).update(dict(js[key]))
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def apply_config(cls, kwargs, conf_dict=None):
|
| 68 |
+
"""Supply default values for kwargs when instantiating class
|
| 69 |
+
|
| 70 |
+
Augments the passed kwargs, by finding entries in the config dict
|
| 71 |
+
which match the classes ``.protocol`` attribute (one or more str)
|
| 72 |
+
|
| 73 |
+
Parameters
|
| 74 |
+
----------
|
| 75 |
+
cls : file system implementation
|
| 76 |
+
kwargs : dict
|
| 77 |
+
conf_dict : dict of dict
|
| 78 |
+
Typically this is the global configuration
|
| 79 |
+
|
| 80 |
+
Returns
|
| 81 |
+
-------
|
| 82 |
+
dict : the modified set of kwargs
|
| 83 |
+
"""
|
| 84 |
+
if conf_dict is None:
|
| 85 |
+
conf_dict = conf
|
| 86 |
+
protos = cls.protocol if isinstance(cls.protocol, (tuple, list)) else [cls.protocol]
|
| 87 |
+
kw = {}
|
| 88 |
+
for proto in protos:
|
| 89 |
+
# default kwargs from the current state of the config
|
| 90 |
+
if proto in conf_dict:
|
| 91 |
+
kw.update(conf_dict[proto])
|
| 92 |
+
# explicit kwargs always win
|
| 93 |
+
kw.update(**kwargs)
|
| 94 |
+
kwargs = kw
|
| 95 |
+
return kwargs
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
set_conf_files(conf_dir, conf)
|
| 99 |
+
set_conf_env(conf)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/conftest.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import shutil
|
| 3 |
+
import subprocess
|
| 4 |
+
import sys
|
| 5 |
+
import time
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
|
| 9 |
+
import fsspec
|
| 10 |
+
from fsspec.implementations.cached import CachingFileSystem
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture()
|
| 14 |
+
def m():
|
| 15 |
+
"""
|
| 16 |
+
Fixture providing a memory filesystem.
|
| 17 |
+
"""
|
| 18 |
+
m = fsspec.filesystem("memory")
|
| 19 |
+
m.store.clear()
|
| 20 |
+
m.pseudo_dirs.clear()
|
| 21 |
+
m.pseudo_dirs.append("")
|
| 22 |
+
try:
|
| 23 |
+
yield m
|
| 24 |
+
finally:
|
| 25 |
+
m.store.clear()
|
| 26 |
+
m.pseudo_dirs.clear()
|
| 27 |
+
m.pseudo_dirs.append("")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@pytest.fixture
|
| 31 |
+
def ftp_writable(tmpdir):
|
| 32 |
+
"""
|
| 33 |
+
Fixture providing a writable FTP filesystem.
|
| 34 |
+
"""
|
| 35 |
+
pytest.importorskip("pyftpdlib")
|
| 36 |
+
from fsspec.implementations.ftp import FTPFileSystem
|
| 37 |
+
|
| 38 |
+
FTPFileSystem.clear_instance_cache() # remove lingering connections
|
| 39 |
+
CachingFileSystem.clear_instance_cache()
|
| 40 |
+
d = str(tmpdir)
|
| 41 |
+
with open(os.path.join(d, "out"), "wb") as f:
|
| 42 |
+
f.write(b"hello" * 10000)
|
| 43 |
+
P = subprocess.Popen(
|
| 44 |
+
[sys.executable, "-m", "pyftpdlib", "-d", d, "-u", "user", "-P", "pass", "-w"]
|
| 45 |
+
)
|
| 46 |
+
try:
|
| 47 |
+
time.sleep(1)
|
| 48 |
+
yield "localhost", 2121, "user", "pass"
|
| 49 |
+
finally:
|
| 50 |
+
P.terminate()
|
| 51 |
+
P.wait()
|
| 52 |
+
try:
|
| 53 |
+
shutil.rmtree(tmpdir)
|
| 54 |
+
except Exception:
|
| 55 |
+
pass
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/core.py
ADDED
|
@@ -0,0 +1,707 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import absolute_import, division, print_function
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from glob import has_magic
|
| 8 |
+
|
| 9 |
+
# for backwards compat, we export cache things from here too
|
| 10 |
+
from .caching import ( # noqa: F401
|
| 11 |
+
BaseCache,
|
| 12 |
+
BlockCache,
|
| 13 |
+
BytesCache,
|
| 14 |
+
MMapCache,
|
| 15 |
+
ReadAheadCache,
|
| 16 |
+
caches,
|
| 17 |
+
)
|
| 18 |
+
from .compression import compr
|
| 19 |
+
from .registry import filesystem, get_filesystem_class
|
| 20 |
+
from .utils import (
|
| 21 |
+
IOWrapper,
|
| 22 |
+
_unstrip_protocol,
|
| 23 |
+
build_name_function,
|
| 24 |
+
infer_compression,
|
| 25 |
+
stringify_path,
|
| 26 |
+
update_storage_options,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger("fsspec")
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class OpenFile(object):
|
| 33 |
+
"""
|
| 34 |
+
File-like object to be used in a context
|
| 35 |
+
|
| 36 |
+
Can layer (buffered) text-mode and compression over any file-system, which
|
| 37 |
+
are typically binary-only.
|
| 38 |
+
|
| 39 |
+
These instances are safe to serialize, as the low-level file object
|
| 40 |
+
is not created until invoked using `with`.
|
| 41 |
+
|
| 42 |
+
Parameters
|
| 43 |
+
----------
|
| 44 |
+
fs: FileSystem
|
| 45 |
+
The file system to use for opening the file. Should be a subclass or duck-type
|
| 46 |
+
with ``fsspec.spec.AbstractFileSystem``
|
| 47 |
+
path: str
|
| 48 |
+
Location to open
|
| 49 |
+
mode: str like 'rb', optional
|
| 50 |
+
Mode of the opened file
|
| 51 |
+
compression: str or None, optional
|
| 52 |
+
Compression to apply
|
| 53 |
+
encoding: str or None, optional
|
| 54 |
+
The encoding to use if opened in text mode.
|
| 55 |
+
errors: str or None, optional
|
| 56 |
+
How to handle encoding errors if opened in text mode.
|
| 57 |
+
newline: None or str
|
| 58 |
+
Passed to TextIOWrapper in text mode, how to handle line endings.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
def __init__(
|
| 62 |
+
self,
|
| 63 |
+
fs,
|
| 64 |
+
path,
|
| 65 |
+
mode="rb",
|
| 66 |
+
compression=None,
|
| 67 |
+
encoding=None,
|
| 68 |
+
errors=None,
|
| 69 |
+
newline=None,
|
| 70 |
+
):
|
| 71 |
+
self.fs = fs
|
| 72 |
+
self.path = path
|
| 73 |
+
self.mode = mode
|
| 74 |
+
self.compression = get_compression(path, compression)
|
| 75 |
+
self.encoding = encoding
|
| 76 |
+
self.errors = errors
|
| 77 |
+
self.newline = newline
|
| 78 |
+
self.fobjects = []
|
| 79 |
+
|
| 80 |
+
def __reduce__(self):
|
| 81 |
+
return (
|
| 82 |
+
OpenFile,
|
| 83 |
+
(
|
| 84 |
+
self.fs,
|
| 85 |
+
self.path,
|
| 86 |
+
self.mode,
|
| 87 |
+
self.compression,
|
| 88 |
+
self.encoding,
|
| 89 |
+
self.errors,
|
| 90 |
+
self.newline,
|
| 91 |
+
),
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
def __repr__(self):
|
| 95 |
+
return "<OpenFile '{}'>".format(self.path)
|
| 96 |
+
|
| 97 |
+
def __fspath__(self):
|
| 98 |
+
# may raise if cannot be resolved to local file
|
| 99 |
+
return self.open().__fspath__()
|
| 100 |
+
|
| 101 |
+
def __enter__(self):
|
| 102 |
+
mode = self.mode.replace("t", "").replace("b", "") + "b"
|
| 103 |
+
|
| 104 |
+
f = self.fs.open(self.path, mode=mode)
|
| 105 |
+
|
| 106 |
+
self.fobjects = [f]
|
| 107 |
+
|
| 108 |
+
if self.compression is not None:
|
| 109 |
+
compress = compr[self.compression]
|
| 110 |
+
f = compress(f, mode=mode[0])
|
| 111 |
+
self.fobjects.append(f)
|
| 112 |
+
|
| 113 |
+
if "b" not in self.mode:
|
| 114 |
+
# assume, for example, that 'r' is equivalent to 'rt' as in builtin
|
| 115 |
+
f = io.TextIOWrapper(
|
| 116 |
+
f, encoding=self.encoding, errors=self.errors, newline=self.newline
|
| 117 |
+
)
|
| 118 |
+
self.fobjects.append(f)
|
| 119 |
+
|
| 120 |
+
return self.fobjects[-1]
|
| 121 |
+
|
| 122 |
+
def __exit__(self, *args):
|
| 123 |
+
self.close()
|
| 124 |
+
|
| 125 |
+
def __del__(self):
|
| 126 |
+
if hasattr(self, "fobjects"):
|
| 127 |
+
self.fobjects.clear() # may cause cleanup of objects and close files
|
| 128 |
+
|
| 129 |
+
@property
|
| 130 |
+
def full_name(self):
|
| 131 |
+
return _unstrip_protocol(self.path, self.fs)
|
| 132 |
+
|
| 133 |
+
def open(self):
|
| 134 |
+
"""Materialise this as a real open file without context
|
| 135 |
+
|
| 136 |
+
The file should be explicitly closed to avoid enclosed file
|
| 137 |
+
instances persisting. This code-path monkey-patches the file-like
|
| 138 |
+
objects, so they can close even if the parent OpenFile object has already
|
| 139 |
+
been deleted; but a with-context is better style.
|
| 140 |
+
"""
|
| 141 |
+
out = self.__enter__()
|
| 142 |
+
closer = out.close
|
| 143 |
+
fobjects = self.fobjects.copy()[:-1]
|
| 144 |
+
mode = self.mode
|
| 145 |
+
|
| 146 |
+
def close():
|
| 147 |
+
# this func has no reference to
|
| 148 |
+
closer() # original close bound method of the final file-like
|
| 149 |
+
_close(fobjects, mode) # call close on other dependent file-likes
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
out.close = close
|
| 153 |
+
except AttributeError:
|
| 154 |
+
out = IOWrapper(out, lambda: _close(fobjects, mode))
|
| 155 |
+
|
| 156 |
+
return out
|
| 157 |
+
|
| 158 |
+
def close(self):
|
| 159 |
+
"""Close all encapsulated file objects"""
|
| 160 |
+
_close(self.fobjects, self.mode)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class OpenFiles(list):
|
| 164 |
+
"""List of OpenFile instances
|
| 165 |
+
|
| 166 |
+
Can be used in a single context, which opens and closes all of the
|
| 167 |
+
contained files. Normal list access to get the elements works as
|
| 168 |
+
normal.
|
| 169 |
+
|
| 170 |
+
A special case is made for caching filesystems - the files will
|
| 171 |
+
be down/uploaded together at the start or end of the context, and
|
| 172 |
+
this may happen concurrently, if the target filesystem supports it.
|
| 173 |
+
"""
|
| 174 |
+
|
| 175 |
+
def __init__(self, *args, mode="rb", fs=None):
|
| 176 |
+
self.mode = mode
|
| 177 |
+
self.fs = fs
|
| 178 |
+
self.files = []
|
| 179 |
+
super().__init__(*args)
|
| 180 |
+
|
| 181 |
+
def __enter__(self):
|
| 182 |
+
if self.fs is None:
|
| 183 |
+
raise ValueError("Context has already been used")
|
| 184 |
+
|
| 185 |
+
fs = self.fs
|
| 186 |
+
while True:
|
| 187 |
+
if hasattr(fs, "open_many"):
|
| 188 |
+
# check for concurrent cache download; or set up for upload
|
| 189 |
+
self.files = fs.open_many(self)
|
| 190 |
+
return self.files
|
| 191 |
+
if hasattr(fs, "fs") and fs.fs is not None:
|
| 192 |
+
fs = fs.fs
|
| 193 |
+
else:
|
| 194 |
+
break
|
| 195 |
+
return [s.__enter__() for s in self]
|
| 196 |
+
|
| 197 |
+
def __exit__(self, *args):
|
| 198 |
+
fs = self.fs
|
| 199 |
+
if "r" not in self.mode:
|
| 200 |
+
while True:
|
| 201 |
+
if hasattr(fs, "open_many"):
|
| 202 |
+
# check for concurrent cache upload
|
| 203 |
+
fs.commit_many(self.files)
|
| 204 |
+
self.files.clear()
|
| 205 |
+
return
|
| 206 |
+
if hasattr(fs, "fs") and fs.fs is not None:
|
| 207 |
+
fs = fs.fs
|
| 208 |
+
else:
|
| 209 |
+
break
|
| 210 |
+
[s.__exit__(*args) for s in self]
|
| 211 |
+
|
| 212 |
+
def __getitem__(self, item):
|
| 213 |
+
out = super().__getitem__(item)
|
| 214 |
+
if isinstance(item, slice):
|
| 215 |
+
return OpenFiles(out, mode=self.mode, fs=self.fs)
|
| 216 |
+
return out
|
| 217 |
+
|
| 218 |
+
def __repr__(self):
|
| 219 |
+
return "<List of %s OpenFile instances>" % len(self)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def _close(fobjects, mode):
|
| 223 |
+
for f in reversed(fobjects):
|
| 224 |
+
if "r" not in mode and not f.closed:
|
| 225 |
+
f.flush()
|
| 226 |
+
f.close()
|
| 227 |
+
fobjects.clear()
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def open_files(
|
| 231 |
+
urlpath,
|
| 232 |
+
mode="rb",
|
| 233 |
+
compression=None,
|
| 234 |
+
encoding="utf8",
|
| 235 |
+
errors=None,
|
| 236 |
+
name_function=None,
|
| 237 |
+
num=1,
|
| 238 |
+
protocol=None,
|
| 239 |
+
newline=None,
|
| 240 |
+
auto_mkdir=True,
|
| 241 |
+
expand=True,
|
| 242 |
+
**kwargs,
|
| 243 |
+
):
|
| 244 |
+
"""Given a path or paths, return a list of ``OpenFile`` objects.
|
| 245 |
+
|
| 246 |
+
For writing, a str path must contain the "*" character, which will be filled
|
| 247 |
+
in by increasing numbers, e.g., "part*" -> "part1", "part2" if num=2.
|
| 248 |
+
|
| 249 |
+
For either reading or writing, can instead provide explicit list of paths.
|
| 250 |
+
|
| 251 |
+
Parameters
|
| 252 |
+
----------
|
| 253 |
+
urlpath: string or list
|
| 254 |
+
Absolute or relative filepath(s). Prefix with a protocol like ``s3://``
|
| 255 |
+
to read from alternative filesystems. To read from multiple files you
|
| 256 |
+
can pass a globstring or a list of paths, with the caveat that they
|
| 257 |
+
must all have the same protocol.
|
| 258 |
+
mode: 'rb', 'wt', etc.
|
| 259 |
+
compression: string or None
|
| 260 |
+
If given, open file using compression codec. Can either be a compression
|
| 261 |
+
name (a key in ``fsspec.compression.compr``) or "infer" to guess the
|
| 262 |
+
compression from the filename suffix.
|
| 263 |
+
encoding: str
|
| 264 |
+
For text mode only
|
| 265 |
+
errors: None or str
|
| 266 |
+
Passed to TextIOWrapper in text mode
|
| 267 |
+
name_function: function or None
|
| 268 |
+
if opening a set of files for writing, those files do not yet exist,
|
| 269 |
+
so we need to generate their names by formatting the urlpath for
|
| 270 |
+
each sequence number
|
| 271 |
+
num: int [1]
|
| 272 |
+
if writing mode, number of files we expect to create (passed to
|
| 273 |
+
name+function)
|
| 274 |
+
protocol: str or None
|
| 275 |
+
If given, overrides the protocol found in the URL.
|
| 276 |
+
newline: bytes or None
|
| 277 |
+
Used for line terminator in text mode. If None, uses system default;
|
| 278 |
+
if blank, uses no translation.
|
| 279 |
+
auto_mkdir: bool (True)
|
| 280 |
+
If in write mode, this will ensure the target directory exists before
|
| 281 |
+
writing, by calling ``fs.mkdirs(exist_ok=True)``.
|
| 282 |
+
expand: bool
|
| 283 |
+
**kwargs: dict
|
| 284 |
+
Extra options that make sense to a particular storage connection, e.g.
|
| 285 |
+
host, port, username, password, etc.
|
| 286 |
+
|
| 287 |
+
Examples
|
| 288 |
+
--------
|
| 289 |
+
>>> files = open_files('2015-*-*.csv') # doctest: +SKIP
|
| 290 |
+
>>> files = open_files(
|
| 291 |
+
... 's3://bucket/2015-*-*.csv.gz', compression='gzip'
|
| 292 |
+
... ) # doctest: +SKIP
|
| 293 |
+
|
| 294 |
+
Returns
|
| 295 |
+
-------
|
| 296 |
+
An ``OpenFiles`` instance, which is a list of ``OpenFile`` objects that can
|
| 297 |
+
be used as a single context
|
| 298 |
+
"""
|
| 299 |
+
fs, fs_token, paths = get_fs_token_paths(
|
| 300 |
+
urlpath,
|
| 301 |
+
mode,
|
| 302 |
+
num=num,
|
| 303 |
+
name_function=name_function,
|
| 304 |
+
storage_options=kwargs,
|
| 305 |
+
protocol=protocol,
|
| 306 |
+
expand=expand,
|
| 307 |
+
)
|
| 308 |
+
if "r" not in mode and auto_mkdir:
|
| 309 |
+
parents = {fs._parent(path) for path in paths}
|
| 310 |
+
[fs.makedirs(parent, exist_ok=True) for parent in parents]
|
| 311 |
+
return OpenFiles(
|
| 312 |
+
[
|
| 313 |
+
OpenFile(
|
| 314 |
+
fs,
|
| 315 |
+
path,
|
| 316 |
+
mode=mode,
|
| 317 |
+
compression=compression,
|
| 318 |
+
encoding=encoding,
|
| 319 |
+
errors=errors,
|
| 320 |
+
newline=newline,
|
| 321 |
+
)
|
| 322 |
+
for path in paths
|
| 323 |
+
],
|
| 324 |
+
mode=mode,
|
| 325 |
+
fs=fs,
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def _un_chain(path, kwargs):
|
| 330 |
+
if isinstance(path, (tuple, list)):
|
| 331 |
+
bits = [_un_chain(p, kwargs) for p in path]
|
| 332 |
+
out = []
|
| 333 |
+
for pbit in zip(*bits):
|
| 334 |
+
paths, protocols, kwargs = zip(*pbit)
|
| 335 |
+
if len(set(protocols)) > 1:
|
| 336 |
+
raise ValueError("Protocol mismatch in URL chain")
|
| 337 |
+
if len(set(paths)) == 1:
|
| 338 |
+
paths = paths[0]
|
| 339 |
+
else:
|
| 340 |
+
paths = list(paths)
|
| 341 |
+
out.append([paths, protocols[0], kwargs[0]])
|
| 342 |
+
return out
|
| 343 |
+
x = re.compile(".*[^a-z]+.*") # test for non protocol-like single word
|
| 344 |
+
bits = (
|
| 345 |
+
[p if "://" in p or x.match(p) else p + "://" for p in path.split("::")]
|
| 346 |
+
if "::" in path
|
| 347 |
+
else [path]
|
| 348 |
+
)
|
| 349 |
+
if len(bits) < 2:
|
| 350 |
+
return []
|
| 351 |
+
# [[url, protocol, kwargs], ...]
|
| 352 |
+
out = []
|
| 353 |
+
previous_bit = None
|
| 354 |
+
for bit in reversed(bits):
|
| 355 |
+
protocol = split_protocol(bit)[0] or "file"
|
| 356 |
+
cls = get_filesystem_class(protocol)
|
| 357 |
+
extra_kwargs = cls._get_kwargs_from_urls(bit)
|
| 358 |
+
kws = kwargs.get(protocol, {})
|
| 359 |
+
kw = dict(**extra_kwargs, **kws)
|
| 360 |
+
bit = cls._strip_protocol(bit)
|
| 361 |
+
if (
|
| 362 |
+
protocol in {"blockcache", "filecache", "simplecache"}
|
| 363 |
+
and "target_protocol" not in kw
|
| 364 |
+
):
|
| 365 |
+
bit = previous_bit
|
| 366 |
+
out.append((bit, protocol, kw))
|
| 367 |
+
previous_bit = bit
|
| 368 |
+
out = list(reversed(out))
|
| 369 |
+
return out
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def url_to_fs(url, **kwargs):
|
| 373 |
+
"""
|
| 374 |
+
Turn fully-qualified and potentially chained URL into filesystem instance
|
| 375 |
+
|
| 376 |
+
Parameters
|
| 377 |
+
----------
|
| 378 |
+
url : str
|
| 379 |
+
The fsspec-compatible URL
|
| 380 |
+
**kwargs: dict
|
| 381 |
+
Extra options that make sense to a particular storage connection, e.g.
|
| 382 |
+
host, port, username, password, etc.
|
| 383 |
+
|
| 384 |
+
Returns
|
| 385 |
+
-------
|
| 386 |
+
filesystem : FileSystem
|
| 387 |
+
The new filesystem discovered from ``url`` and created with
|
| 388 |
+
``**kwargs``.
|
| 389 |
+
urlpath : str
|
| 390 |
+
The file-systems-specific URL for ``url``.
|
| 391 |
+
"""
|
| 392 |
+
chain = _un_chain(url, kwargs)
|
| 393 |
+
if len(chain) > 1:
|
| 394 |
+
inkwargs = {}
|
| 395 |
+
# Reverse iterate the chain, creating a nested target_* structure
|
| 396 |
+
for i, ch in enumerate(reversed(chain)):
|
| 397 |
+
urls, protocol, kw = ch
|
| 398 |
+
if i == len(chain) - 1:
|
| 399 |
+
inkwargs = dict(**kw, **inkwargs)
|
| 400 |
+
continue
|
| 401 |
+
inkwargs["target_options"] = dict(**kw, **inkwargs)
|
| 402 |
+
inkwargs["target_protocol"] = protocol
|
| 403 |
+
inkwargs["fo"] = urls
|
| 404 |
+
urlpath, protocol, _ = chain[0]
|
| 405 |
+
fs = filesystem(protocol, **inkwargs)
|
| 406 |
+
else:
|
| 407 |
+
protocol = split_protocol(url)[0]
|
| 408 |
+
cls = get_filesystem_class(protocol)
|
| 409 |
+
|
| 410 |
+
options = cls._get_kwargs_from_urls(url)
|
| 411 |
+
update_storage_options(options, kwargs)
|
| 412 |
+
fs = cls(**options)
|
| 413 |
+
urlpath = fs._strip_protocol(url)
|
| 414 |
+
return fs, urlpath
|
| 415 |
+
|
| 416 |
+
|
| 417 |
+
def open(
|
| 418 |
+
urlpath,
|
| 419 |
+
mode="rb",
|
| 420 |
+
compression=None,
|
| 421 |
+
encoding="utf8",
|
| 422 |
+
errors=None,
|
| 423 |
+
protocol=None,
|
| 424 |
+
newline=None,
|
| 425 |
+
**kwargs,
|
| 426 |
+
):
|
| 427 |
+
"""Given a path or paths, return one ``OpenFile`` object.
|
| 428 |
+
|
| 429 |
+
Parameters
|
| 430 |
+
----------
|
| 431 |
+
urlpath: string or list
|
| 432 |
+
Absolute or relative filepath. Prefix with a protocol like ``s3://``
|
| 433 |
+
to read from alternative filesystems. Should not include glob
|
| 434 |
+
character(s).
|
| 435 |
+
mode: 'rb', 'wt', etc.
|
| 436 |
+
compression: string or None
|
| 437 |
+
If given, open file using compression codec. Can either be a compression
|
| 438 |
+
name (a key in ``fsspec.compression.compr``) or "infer" to guess the
|
| 439 |
+
compression from the filename suffix.
|
| 440 |
+
encoding: str
|
| 441 |
+
For text mode only
|
| 442 |
+
errors: None or str
|
| 443 |
+
Passed to TextIOWrapper in text mode
|
| 444 |
+
protocol: str or None
|
| 445 |
+
If given, overrides the protocol found in the URL.
|
| 446 |
+
newline: bytes or None
|
| 447 |
+
Used for line terminator in text mode. If None, uses system default;
|
| 448 |
+
if blank, uses no translation.
|
| 449 |
+
**kwargs: dict
|
| 450 |
+
Extra options that make sense to a particular storage connection, e.g.
|
| 451 |
+
host, port, username, password, etc.
|
| 452 |
+
|
| 453 |
+
Examples
|
| 454 |
+
--------
|
| 455 |
+
>>> openfile = open('2015-01-01.csv') # doctest: +SKIP
|
| 456 |
+
>>> openfile = open(
|
| 457 |
+
... 's3://bucket/2015-01-01.csv.gz', compression='gzip'
|
| 458 |
+
... ) # doctest: +SKIP
|
| 459 |
+
>>> with openfile as f:
|
| 460 |
+
... df = pd.read_csv(f) # doctest: +SKIP
|
| 461 |
+
...
|
| 462 |
+
|
| 463 |
+
Returns
|
| 464 |
+
-------
|
| 465 |
+
``OpenFile`` object.
|
| 466 |
+
"""
|
| 467 |
+
return open_files(
|
| 468 |
+
urlpath=[urlpath],
|
| 469 |
+
mode=mode,
|
| 470 |
+
compression=compression,
|
| 471 |
+
encoding=encoding,
|
| 472 |
+
errors=errors,
|
| 473 |
+
protocol=protocol,
|
| 474 |
+
newline=newline,
|
| 475 |
+
expand=False,
|
| 476 |
+
**kwargs,
|
| 477 |
+
)[0]
|
| 478 |
+
|
| 479 |
+
|
| 480 |
+
def open_local(url, mode="rb", **storage_options):
|
| 481 |
+
"""Open file(s) which can be resolved to local
|
| 482 |
+
|
| 483 |
+
For files which either are local, or get downloaded upon open
|
| 484 |
+
(e.g., by file caching)
|
| 485 |
+
|
| 486 |
+
Parameters
|
| 487 |
+
----------
|
| 488 |
+
url: str or list(str)
|
| 489 |
+
mode: str
|
| 490 |
+
Must be read mode
|
| 491 |
+
storage_options:
|
| 492 |
+
passed on to FS for or used by open_files (e.g., compression)
|
| 493 |
+
"""
|
| 494 |
+
if "r" not in mode:
|
| 495 |
+
raise ValueError("Can only ensure local files when reading")
|
| 496 |
+
of = open_files(url, mode=mode, **storage_options)
|
| 497 |
+
if not getattr(of[0].fs, "local_file", False):
|
| 498 |
+
raise ValueError(
|
| 499 |
+
"open_local can only be used on a filesystem which"
|
| 500 |
+
" has attribute local_file=True"
|
| 501 |
+
)
|
| 502 |
+
with of as files:
|
| 503 |
+
paths = [f.name for f in files]
|
| 504 |
+
if isinstance(url, str) and not has_magic(url):
|
| 505 |
+
return paths[0]
|
| 506 |
+
return paths
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def get_compression(urlpath, compression):
|
| 510 |
+
if compression == "infer":
|
| 511 |
+
compression = infer_compression(urlpath)
|
| 512 |
+
if compression is not None and compression not in compr:
|
| 513 |
+
raise ValueError("Compression type %s not supported" % compression)
|
| 514 |
+
return compression
|
| 515 |
+
|
| 516 |
+
|
| 517 |
+
def split_protocol(urlpath):
|
| 518 |
+
"""Return protocol, path pair"""
|
| 519 |
+
urlpath = stringify_path(urlpath)
|
| 520 |
+
if "://" in urlpath:
|
| 521 |
+
protocol, path = urlpath.split("://", 1)
|
| 522 |
+
if len(protocol) > 1:
|
| 523 |
+
# excludes Windows paths
|
| 524 |
+
return protocol, path
|
| 525 |
+
return None, urlpath
|
| 526 |
+
|
| 527 |
+
|
| 528 |
+
def strip_protocol(urlpath):
|
| 529 |
+
"""Return only path part of full URL, according to appropriate backend"""
|
| 530 |
+
protocol, _ = split_protocol(urlpath)
|
| 531 |
+
cls = get_filesystem_class(protocol)
|
| 532 |
+
return cls._strip_protocol(urlpath)
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def expand_paths_if_needed(paths, mode, num, fs, name_function):
|
| 536 |
+
"""Expand paths if they have a ``*`` in them (write mode) or any of ``*?[]``
|
| 537 |
+
in them (read mode).
|
| 538 |
+
|
| 539 |
+
:param paths: list of paths
|
| 540 |
+
mode: str
|
| 541 |
+
Mode in which to open files.
|
| 542 |
+
num: int
|
| 543 |
+
If opening in writing mode, number of files we expect to create.
|
| 544 |
+
fs: filesystem object
|
| 545 |
+
name_function: callable
|
| 546 |
+
If opening in writing mode, this callable is used to generate path
|
| 547 |
+
names. Names are generated for each partition by
|
| 548 |
+
``urlpath.replace('*', name_function(partition_index))``.
|
| 549 |
+
:return: list of paths
|
| 550 |
+
"""
|
| 551 |
+
expanded_paths = []
|
| 552 |
+
paths = list(paths)
|
| 553 |
+
|
| 554 |
+
if "w" in mode: # read mode
|
| 555 |
+
if sum([1 for p in paths if "*" in p]) > 1:
|
| 556 |
+
raise ValueError(
|
| 557 |
+
"When writing data, only one filename mask can be specified."
|
| 558 |
+
)
|
| 559 |
+
num = max(num, len(paths))
|
| 560 |
+
|
| 561 |
+
for curr_path in paths:
|
| 562 |
+
if "*" in curr_path:
|
| 563 |
+
# expand using name_function
|
| 564 |
+
expanded_paths.extend(_expand_paths(curr_path, name_function, num))
|
| 565 |
+
else:
|
| 566 |
+
expanded_paths.append(curr_path)
|
| 567 |
+
# if we generated more paths that asked for, trim the list
|
| 568 |
+
if len(expanded_paths) > num:
|
| 569 |
+
expanded_paths = expanded_paths[:num]
|
| 570 |
+
|
| 571 |
+
else: # read mode
|
| 572 |
+
for curr_path in paths:
|
| 573 |
+
if has_magic(curr_path):
|
| 574 |
+
# expand using glob
|
| 575 |
+
expanded_paths.extend(fs.glob(curr_path))
|
| 576 |
+
else:
|
| 577 |
+
expanded_paths.append(curr_path)
|
| 578 |
+
|
| 579 |
+
return expanded_paths
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
def get_fs_token_paths(
|
| 583 |
+
urlpath,
|
| 584 |
+
mode="rb",
|
| 585 |
+
num=1,
|
| 586 |
+
name_function=None,
|
| 587 |
+
storage_options=None,
|
| 588 |
+
protocol=None,
|
| 589 |
+
expand=True,
|
| 590 |
+
):
|
| 591 |
+
"""Filesystem, deterministic token, and paths from a urlpath and options.
|
| 592 |
+
|
| 593 |
+
Parameters
|
| 594 |
+
----------
|
| 595 |
+
urlpath: string or iterable
|
| 596 |
+
Absolute or relative filepath, URL (may include protocols like
|
| 597 |
+
``s3://``), or globstring pointing to data.
|
| 598 |
+
mode: str, optional
|
| 599 |
+
Mode in which to open files.
|
| 600 |
+
num: int, optional
|
| 601 |
+
If opening in writing mode, number of files we expect to create.
|
| 602 |
+
name_function: callable, optional
|
| 603 |
+
If opening in writing mode, this callable is used to generate path
|
| 604 |
+
names. Names are generated for each partition by
|
| 605 |
+
``urlpath.replace('*', name_function(partition_index))``.
|
| 606 |
+
storage_options: dict, optional
|
| 607 |
+
Additional keywords to pass to the filesystem class.
|
| 608 |
+
protocol: str or None
|
| 609 |
+
To override the protocol specifier in the URL
|
| 610 |
+
expand: bool
|
| 611 |
+
Expand string paths for writing, assuming the path is a directory
|
| 612 |
+
"""
|
| 613 |
+
if isinstance(urlpath, (list, tuple, set)):
|
| 614 |
+
if not urlpath:
|
| 615 |
+
raise ValueError("empty urlpath sequence")
|
| 616 |
+
urlpath = [stringify_path(u) for u in urlpath]
|
| 617 |
+
else:
|
| 618 |
+
urlpath = stringify_path(urlpath)
|
| 619 |
+
chain = _un_chain(urlpath, storage_options or {})
|
| 620 |
+
if len(chain) > 1:
|
| 621 |
+
inkwargs = {}
|
| 622 |
+
# Reverse iterate the chain, creating a nested target_* structure
|
| 623 |
+
for i, ch in enumerate(reversed(chain)):
|
| 624 |
+
urls, nested_protocol, kw = ch
|
| 625 |
+
if i == len(chain) - 1:
|
| 626 |
+
inkwargs = dict(**kw, **inkwargs)
|
| 627 |
+
continue
|
| 628 |
+
inkwargs["target_options"] = dict(**kw, **inkwargs)
|
| 629 |
+
inkwargs["target_protocol"] = nested_protocol
|
| 630 |
+
inkwargs["fo"] = urls
|
| 631 |
+
paths, protocol, _ = chain[0]
|
| 632 |
+
fs = filesystem(protocol, **inkwargs)
|
| 633 |
+
if isinstance(paths, (list, tuple, set)):
|
| 634 |
+
paths = [fs._strip_protocol(u) for u in paths]
|
| 635 |
+
else:
|
| 636 |
+
paths = fs._strip_protocol(paths)
|
| 637 |
+
else:
|
| 638 |
+
if isinstance(urlpath, (list, tuple, set)):
|
| 639 |
+
protocols, paths = zip(*map(split_protocol, urlpath))
|
| 640 |
+
if protocol is None:
|
| 641 |
+
protocol = protocols[0]
|
| 642 |
+
if not all(p == protocol for p in protocols):
|
| 643 |
+
raise ValueError(
|
| 644 |
+
"When specifying a list of paths, all paths must "
|
| 645 |
+
"share the same protocol"
|
| 646 |
+
)
|
| 647 |
+
cls = get_filesystem_class(protocol)
|
| 648 |
+
optionss = list(map(cls._get_kwargs_from_urls, urlpath))
|
| 649 |
+
paths = [cls._strip_protocol(u) for u in urlpath]
|
| 650 |
+
options = optionss[0]
|
| 651 |
+
if not all(o == options for o in optionss):
|
| 652 |
+
raise ValueError(
|
| 653 |
+
"When specifying a list of paths, all paths must "
|
| 654 |
+
"share the same file-system options"
|
| 655 |
+
)
|
| 656 |
+
update_storage_options(options, storage_options)
|
| 657 |
+
fs = cls(**options)
|
| 658 |
+
else:
|
| 659 |
+
protocols = split_protocol(urlpath)[0]
|
| 660 |
+
protocol = protocol or protocols
|
| 661 |
+
cls = get_filesystem_class(protocol)
|
| 662 |
+
options = cls._get_kwargs_from_urls(urlpath)
|
| 663 |
+
paths = cls._strip_protocol(urlpath)
|
| 664 |
+
update_storage_options(options, storage_options)
|
| 665 |
+
fs = cls(**options)
|
| 666 |
+
|
| 667 |
+
if isinstance(paths, (list, tuple, set)):
|
| 668 |
+
paths = expand_paths_if_needed(paths, mode, num, fs, name_function)
|
| 669 |
+
else:
|
| 670 |
+
if "w" in mode and expand:
|
| 671 |
+
paths = _expand_paths(paths, name_function, num)
|
| 672 |
+
elif "*" in paths:
|
| 673 |
+
paths = [f for f in sorted(fs.glob(paths)) if not fs.isdir(f)]
|
| 674 |
+
else:
|
| 675 |
+
paths = [paths]
|
| 676 |
+
|
| 677 |
+
return fs, fs._fs_token, paths
|
| 678 |
+
|
| 679 |
+
|
| 680 |
+
def _expand_paths(path, name_function, num):
|
| 681 |
+
if isinstance(path, str):
|
| 682 |
+
if path.count("*") > 1:
|
| 683 |
+
raise ValueError("Output path spec must contain exactly one '*'.")
|
| 684 |
+
elif "*" not in path:
|
| 685 |
+
path = os.path.join(path, "*.part")
|
| 686 |
+
|
| 687 |
+
if name_function is None:
|
| 688 |
+
name_function = build_name_function(num - 1)
|
| 689 |
+
|
| 690 |
+
paths = [path.replace("*", name_function(i)) for i in range(num)]
|
| 691 |
+
if paths != sorted(paths):
|
| 692 |
+
logger.warning(
|
| 693 |
+
"In order to preserve order between partitions"
|
| 694 |
+
" paths created with ``name_function`` should "
|
| 695 |
+
"sort to partition order"
|
| 696 |
+
)
|
| 697 |
+
elif isinstance(path, (tuple, list)):
|
| 698 |
+
assert len(path) == num
|
| 699 |
+
paths = list(path)
|
| 700 |
+
else:
|
| 701 |
+
raise ValueError(
|
| 702 |
+
"Path should be either\n"
|
| 703 |
+
"1. A list of paths: ['foo.json', 'bar.json', ...]\n"
|
| 704 |
+
"2. A directory: 'foo/\n"
|
| 705 |
+
"3. A path with a '*' in it: 'foo.*.json'"
|
| 706 |
+
)
|
| 707 |
+
return paths
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/dircache.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from collections.abc import MutableMapping
|
| 3 |
+
from functools import lru_cache
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class DirCache(MutableMapping):
|
| 7 |
+
"""
|
| 8 |
+
Caching of directory listings, in a structure like::
|
| 9 |
+
|
| 10 |
+
{"path0": [
|
| 11 |
+
{"name": "path0/file0",
|
| 12 |
+
"size": 123,
|
| 13 |
+
"type": "file",
|
| 14 |
+
...
|
| 15 |
+
},
|
| 16 |
+
{"name": "path0/file1",
|
| 17 |
+
},
|
| 18 |
+
...
|
| 19 |
+
],
|
| 20 |
+
"path1": [...]
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
Parameters to this class control listing expiry or indeed turn
|
| 24 |
+
caching off
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
def __init__(
|
| 28 |
+
self,
|
| 29 |
+
use_listings_cache=True,
|
| 30 |
+
listings_expiry_time=None,
|
| 31 |
+
max_paths=None,
|
| 32 |
+
**kwargs,
|
| 33 |
+
):
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
Parameters
|
| 37 |
+
----------
|
| 38 |
+
use_listings_cache: bool
|
| 39 |
+
If False, this cache never returns items, but always reports KeyError,
|
| 40 |
+
and setting items has no effect
|
| 41 |
+
listings_expiry_time: int or float (optional)
|
| 42 |
+
Time in seconds that a listing is considered valid. If None,
|
| 43 |
+
listings do not expire.
|
| 44 |
+
max_paths: int (optional)
|
| 45 |
+
The number of most recent listings that are considered valid; 'recent'
|
| 46 |
+
refers to when the entry was set.
|
| 47 |
+
"""
|
| 48 |
+
self._cache = {}
|
| 49 |
+
self._times = {}
|
| 50 |
+
if max_paths:
|
| 51 |
+
self._q = lru_cache(max_paths + 1)(lambda key: self._cache.pop(key, None))
|
| 52 |
+
self.use_listings_cache = use_listings_cache
|
| 53 |
+
self.listings_expiry_time = listings_expiry_time
|
| 54 |
+
self.max_paths = max_paths
|
| 55 |
+
|
| 56 |
+
def __getitem__(self, item):
|
| 57 |
+
if self.listings_expiry_time is not None:
|
| 58 |
+
if self._times.get(item, 0) - time.time() < -self.listings_expiry_time:
|
| 59 |
+
del self._cache[item]
|
| 60 |
+
if self.max_paths:
|
| 61 |
+
self._q(item)
|
| 62 |
+
return self._cache[item] # maybe raises KeyError
|
| 63 |
+
|
| 64 |
+
def clear(self):
|
| 65 |
+
self._cache.clear()
|
| 66 |
+
|
| 67 |
+
def __len__(self):
|
| 68 |
+
return len(self._cache)
|
| 69 |
+
|
| 70 |
+
def __contains__(self, item):
|
| 71 |
+
try:
|
| 72 |
+
self[item]
|
| 73 |
+
return True
|
| 74 |
+
except KeyError:
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
def __setitem__(self, key, value):
|
| 78 |
+
if not self.use_listings_cache:
|
| 79 |
+
return
|
| 80 |
+
if self.max_paths:
|
| 81 |
+
self._q(key)
|
| 82 |
+
self._cache[key] = value
|
| 83 |
+
if self.listings_expiry_time is not None:
|
| 84 |
+
self._times[key] = time.time()
|
| 85 |
+
|
| 86 |
+
def __delitem__(self, key):
|
| 87 |
+
del self._cache[key]
|
| 88 |
+
|
| 89 |
+
def __iter__(self):
|
| 90 |
+
return (k for k in self._cache if k in self)
|
| 91 |
+
|
| 92 |
+
def __reduce__(self):
|
| 93 |
+
return (
|
| 94 |
+
DirCache,
|
| 95 |
+
(self.use_listings_cache, self.listings_expiry_time, self.max_paths),
|
| 96 |
+
)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/exceptions.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
fsspec user-defined exception classes
|
| 3 |
+
"""
|
| 4 |
+
import asyncio
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class BlocksizeMismatchError(ValueError):
|
| 8 |
+
"""
|
| 9 |
+
Raised when a cached file is opened with a different blocksize than it was
|
| 10 |
+
written with
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
...
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class FSTimeoutError(asyncio.TimeoutError):
|
| 17 |
+
"""
|
| 18 |
+
Raised when a fsspec function timed out occurs
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
...
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/fuse.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import stat
|
| 5 |
+
import threading
|
| 6 |
+
import time
|
| 7 |
+
from errno import EIO, ENOENT
|
| 8 |
+
|
| 9 |
+
from fuse import FUSE, FuseOSError, LoggingMixIn, Operations
|
| 10 |
+
|
| 11 |
+
from fsspec import __version__
|
| 12 |
+
from fsspec.core import url_to_fs
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger("fsspec.fuse")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class FUSEr(Operations):
|
| 18 |
+
def __init__(self, fs, path, ready_file=False):
|
| 19 |
+
self.fs = fs
|
| 20 |
+
self.cache = {}
|
| 21 |
+
self.root = path.rstrip("/") + "/"
|
| 22 |
+
self.counter = 0
|
| 23 |
+
logger.info("Starting FUSE at %s", path)
|
| 24 |
+
self._ready_file = ready_file
|
| 25 |
+
|
| 26 |
+
def getattr(self, path, fh=None):
|
| 27 |
+
logger.debug("getattr %s", path)
|
| 28 |
+
if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]:
|
| 29 |
+
return {"type": "file", "st_size": 5}
|
| 30 |
+
|
| 31 |
+
path = "".join([self.root, path.lstrip("/")]).rstrip("/")
|
| 32 |
+
try:
|
| 33 |
+
info = self.fs.info(path)
|
| 34 |
+
except FileNotFoundError:
|
| 35 |
+
raise FuseOSError(ENOENT)
|
| 36 |
+
|
| 37 |
+
data = {"st_uid": info.get("uid", 1000), "st_gid": info.get("gid", 1000)}
|
| 38 |
+
perm = info.get("mode", 0o777)
|
| 39 |
+
|
| 40 |
+
if info["type"] != "file":
|
| 41 |
+
data["st_mode"] = stat.S_IFDIR | perm
|
| 42 |
+
data["st_size"] = 0
|
| 43 |
+
data["st_blksize"] = 0
|
| 44 |
+
else:
|
| 45 |
+
data["st_mode"] = stat.S_IFREG | perm
|
| 46 |
+
data["st_size"] = info["size"]
|
| 47 |
+
data["st_blksize"] = 5 * 2**20
|
| 48 |
+
data["st_nlink"] = 1
|
| 49 |
+
data["st_atime"] = time.time()
|
| 50 |
+
data["st_ctime"] = time.time()
|
| 51 |
+
data["st_mtime"] = time.time()
|
| 52 |
+
return data
|
| 53 |
+
|
| 54 |
+
def readdir(self, path, fh):
|
| 55 |
+
logger.debug("readdir %s", path)
|
| 56 |
+
path = "".join([self.root, path.lstrip("/")])
|
| 57 |
+
files = self.fs.ls(path, False)
|
| 58 |
+
files = [os.path.basename(f.rstrip("/")) for f in files]
|
| 59 |
+
return [".", ".."] + files
|
| 60 |
+
|
| 61 |
+
def mkdir(self, path, mode):
|
| 62 |
+
path = "".join([self.root, path.lstrip("/")])
|
| 63 |
+
self.fs.mkdir(path)
|
| 64 |
+
return 0
|
| 65 |
+
|
| 66 |
+
def rmdir(self, path):
|
| 67 |
+
path = "".join([self.root, path.lstrip("/")])
|
| 68 |
+
self.fs.rmdir(path)
|
| 69 |
+
return 0
|
| 70 |
+
|
| 71 |
+
def read(self, path, size, offset, fh):
|
| 72 |
+
logger.debug("read %s", (path, size, offset))
|
| 73 |
+
if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]:
|
| 74 |
+
# status indicator
|
| 75 |
+
return b"ready"
|
| 76 |
+
|
| 77 |
+
f = self.cache[fh]
|
| 78 |
+
f.seek(offset)
|
| 79 |
+
out = f.read(size)
|
| 80 |
+
return out
|
| 81 |
+
|
| 82 |
+
def write(self, path, data, offset, fh):
|
| 83 |
+
logger.debug("write %s", (path, offset))
|
| 84 |
+
f = self.cache[fh]
|
| 85 |
+
f.seek(offset)
|
| 86 |
+
f.write(data)
|
| 87 |
+
return len(data)
|
| 88 |
+
|
| 89 |
+
def create(self, path, flags, fi=None):
|
| 90 |
+
logger.debug("create %s", (path, flags))
|
| 91 |
+
fn = "".join([self.root, path.lstrip("/")])
|
| 92 |
+
self.fs.touch(fn) # OS will want to get attributes immediately
|
| 93 |
+
f = self.fs.open(fn, "wb")
|
| 94 |
+
self.cache[self.counter] = f
|
| 95 |
+
self.counter += 1
|
| 96 |
+
return self.counter - 1
|
| 97 |
+
|
| 98 |
+
def open(self, path, flags):
|
| 99 |
+
logger.debug("open %s", (path, flags))
|
| 100 |
+
fn = "".join([self.root, path.lstrip("/")])
|
| 101 |
+
if flags % 2 == 0:
|
| 102 |
+
# read
|
| 103 |
+
mode = "rb"
|
| 104 |
+
else:
|
| 105 |
+
# write/create
|
| 106 |
+
mode = "wb"
|
| 107 |
+
self.cache[self.counter] = self.fs.open(fn, mode)
|
| 108 |
+
self.counter += 1
|
| 109 |
+
return self.counter - 1
|
| 110 |
+
|
| 111 |
+
def truncate(self, path, length, fh=None):
|
| 112 |
+
fn = "".join([self.root, path.lstrip("/")])
|
| 113 |
+
if length != 0:
|
| 114 |
+
raise NotImplementedError
|
| 115 |
+
# maybe should be no-op since open with write sets size to zero anyway
|
| 116 |
+
self.fs.touch(fn)
|
| 117 |
+
|
| 118 |
+
def unlink(self, path):
|
| 119 |
+
fn = "".join([self.root, path.lstrip("/")])
|
| 120 |
+
try:
|
| 121 |
+
self.fs.rm(fn, False)
|
| 122 |
+
except (IOError, FileNotFoundError):
|
| 123 |
+
raise FuseOSError(EIO)
|
| 124 |
+
|
| 125 |
+
def release(self, path, fh):
|
| 126 |
+
try:
|
| 127 |
+
if fh in self.cache:
|
| 128 |
+
f = self.cache[fh]
|
| 129 |
+
f.close()
|
| 130 |
+
self.cache.pop(fh)
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(e)
|
| 133 |
+
return 0
|
| 134 |
+
|
| 135 |
+
def chmod(self, path, mode):
|
| 136 |
+
if hasattr(self.fs, "chmod"):
|
| 137 |
+
path = "".join([self.root, path.lstrip("/")])
|
| 138 |
+
return self.fs.chmod(path, mode)
|
| 139 |
+
raise NotImplementedError
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def run(
|
| 143 |
+
fs,
|
| 144 |
+
path,
|
| 145 |
+
mount_point,
|
| 146 |
+
foreground=True,
|
| 147 |
+
threads=False,
|
| 148 |
+
ready_file=False,
|
| 149 |
+
ops_class=FUSEr,
|
| 150 |
+
):
|
| 151 |
+
"""Mount stuff in a local directory
|
| 152 |
+
|
| 153 |
+
This uses fusepy to make it appear as if a given path on an fsspec
|
| 154 |
+
instance is in fact resident within the local file-system.
|
| 155 |
+
|
| 156 |
+
This requires that fusepy by installed, and that FUSE be available on
|
| 157 |
+
the system (typically requiring a package to be installed with
|
| 158 |
+
apt, yum, brew, etc.).
|
| 159 |
+
|
| 160 |
+
Parameters
|
| 161 |
+
----------
|
| 162 |
+
fs: file-system instance
|
| 163 |
+
From one of the compatible implementations
|
| 164 |
+
path: str
|
| 165 |
+
Location on that file-system to regard as the root directory to
|
| 166 |
+
mount. Note that you typically should include the terminating "/"
|
| 167 |
+
character.
|
| 168 |
+
mount_point: str
|
| 169 |
+
An empty directory on the local file-system where the contents of
|
| 170 |
+
the remote path will appear.
|
| 171 |
+
foreground: bool
|
| 172 |
+
Whether or not calling this function will block. Operation will
|
| 173 |
+
typically be more stable if True.
|
| 174 |
+
threads: bool
|
| 175 |
+
Whether or not to create threads when responding to file operations
|
| 176 |
+
within the mounter directory. Operation will typically be more
|
| 177 |
+
stable if False.
|
| 178 |
+
ready_file: bool
|
| 179 |
+
Whether the FUSE process is ready. The `.fuse_ready` file will
|
| 180 |
+
exist in the `mount_point` directory if True. Debugging purpose.
|
| 181 |
+
ops_class: FUSEr or Subclass of FUSEr
|
| 182 |
+
To override the default behavior of FUSEr. For Example, logging
|
| 183 |
+
to file.
|
| 184 |
+
|
| 185 |
+
"""
|
| 186 |
+
func = lambda: FUSE(
|
| 187 |
+
ops_class(fs, path, ready_file=ready_file),
|
| 188 |
+
mount_point,
|
| 189 |
+
nothreads=not threads,
|
| 190 |
+
foreground=foreground,
|
| 191 |
+
)
|
| 192 |
+
if not foreground:
|
| 193 |
+
th = threading.Thread(target=func)
|
| 194 |
+
th.daemon = True
|
| 195 |
+
th.start()
|
| 196 |
+
return th
|
| 197 |
+
else: # pragma: no cover
|
| 198 |
+
try:
|
| 199 |
+
func()
|
| 200 |
+
except KeyboardInterrupt:
|
| 201 |
+
pass
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def main(args):
|
| 205 |
+
"""Mount filesystem from chained URL to MOUNT_POINT.
|
| 206 |
+
|
| 207 |
+
Examples:
|
| 208 |
+
|
| 209 |
+
python3 -m fsspec.fuse memory /usr/share /tmp/mem
|
| 210 |
+
|
| 211 |
+
python3 -m fsspec.fuse local /tmp/source /tmp/local \\
|
| 212 |
+
-l /tmp/fsspecfuse.log
|
| 213 |
+
|
| 214 |
+
You can also mount chained-URLs and use special settings:
|
| 215 |
+
|
| 216 |
+
python3 -m fsspec.fuse 'filecache::zip::file://data.zip' \\
|
| 217 |
+
/ /tmp/zip \\
|
| 218 |
+
-o 'filecache-cache_storage=/tmp/simplecache'
|
| 219 |
+
|
| 220 |
+
You can specify the type of the setting by using `[int]` or `[bool]`,
|
| 221 |
+
(`true`, `yes`, `1` represents the Boolean value `True`):
|
| 222 |
+
|
| 223 |
+
python3 -m fsspec.fuse 'simplecache::ftp://ftp1.at.proftpd.org' \\
|
| 224 |
+
/historic/packages/RPMS /tmp/ftp \\
|
| 225 |
+
-o 'simplecache-cache_storage=/tmp/simplecache' \\
|
| 226 |
+
-o 'simplecache-check_files=false[bool]' \\
|
| 227 |
+
-o 'ftp-listings_expiry_time=60[int]' \\
|
| 228 |
+
-o 'ftp-username=anonymous' \\
|
| 229 |
+
-o 'ftp-password=xieyanbo'
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
class RawDescriptionArgumentParser(argparse.ArgumentParser):
|
| 233 |
+
def format_help(self):
|
| 234 |
+
usage = super(RawDescriptionArgumentParser, self).format_help()
|
| 235 |
+
parts = usage.split("\n\n")
|
| 236 |
+
parts[1] = self.description.rstrip()
|
| 237 |
+
return "\n\n".join(parts)
|
| 238 |
+
|
| 239 |
+
parser = RawDescriptionArgumentParser(prog="fsspec.fuse", description=main.__doc__)
|
| 240 |
+
parser.add_argument("--version", action="version", version=__version__)
|
| 241 |
+
parser.add_argument("url", type=str, help="fs url")
|
| 242 |
+
parser.add_argument("source_path", type=str, help="source directory in fs")
|
| 243 |
+
parser.add_argument("mount_point", type=str, help="local directory")
|
| 244 |
+
parser.add_argument(
|
| 245 |
+
"-o",
|
| 246 |
+
"--option",
|
| 247 |
+
action="append",
|
| 248 |
+
help="Any options of protocol included in the chained URL",
|
| 249 |
+
)
|
| 250 |
+
parser.add_argument(
|
| 251 |
+
"-l", "--log-file", type=str, help="Logging FUSE debug info (Default: '')"
|
| 252 |
+
)
|
| 253 |
+
parser.add_argument(
|
| 254 |
+
"-f",
|
| 255 |
+
"--foreground",
|
| 256 |
+
action="store_false",
|
| 257 |
+
help="Running in foreground or not (Default: False)",
|
| 258 |
+
)
|
| 259 |
+
parser.add_argument(
|
| 260 |
+
"-t",
|
| 261 |
+
"--threads",
|
| 262 |
+
action="store_false",
|
| 263 |
+
help="Running with threads support (Default: False)",
|
| 264 |
+
)
|
| 265 |
+
parser.add_argument(
|
| 266 |
+
"-r",
|
| 267 |
+
"--ready-file",
|
| 268 |
+
action="store_false",
|
| 269 |
+
help="The `.fuse_ready` file will exist after FUSE is ready. "
|
| 270 |
+
"(Debugging purpose, Default: False)",
|
| 271 |
+
)
|
| 272 |
+
args = parser.parse_args(args)
|
| 273 |
+
|
| 274 |
+
kwargs = {}
|
| 275 |
+
for item in args.option or []:
|
| 276 |
+
key, sep, value = item.partition("=")
|
| 277 |
+
if not sep:
|
| 278 |
+
parser.error(message="Wrong option: {!r}".format(item))
|
| 279 |
+
val = value.lower()
|
| 280 |
+
if val.endswith("[int]"):
|
| 281 |
+
value = int(value[: -len("[int]")])
|
| 282 |
+
elif val.endswith("[bool]"):
|
| 283 |
+
value = val[: -len("[bool]")] in ["1", "yes", "true"]
|
| 284 |
+
|
| 285 |
+
if "-" in key:
|
| 286 |
+
fs_name, setting_name = key.split("-", 1)
|
| 287 |
+
if fs_name in kwargs:
|
| 288 |
+
kwargs[fs_name][setting_name] = value
|
| 289 |
+
else:
|
| 290 |
+
kwargs[fs_name] = {setting_name: value}
|
| 291 |
+
else:
|
| 292 |
+
kwargs[key] = value
|
| 293 |
+
|
| 294 |
+
if args.log_file:
|
| 295 |
+
logging.basicConfig(
|
| 296 |
+
level=logging.DEBUG,
|
| 297 |
+
filename=args.log_file,
|
| 298 |
+
format="%(asctime)s %(message)s",
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
class LoggingFUSEr(FUSEr, LoggingMixIn):
|
| 302 |
+
pass
|
| 303 |
+
|
| 304 |
+
fuser = LoggingFUSEr
|
| 305 |
+
else:
|
| 306 |
+
fuser = FUSEr
|
| 307 |
+
|
| 308 |
+
fs, url_path = url_to_fs(args.url, **kwargs)
|
| 309 |
+
logger.debug("Mounting %s to %s", url_path, str(args.mount_point))
|
| 310 |
+
run(
|
| 311 |
+
fs,
|
| 312 |
+
args.source_path,
|
| 313 |
+
args.mount_point,
|
| 314 |
+
foreground=args.foreground,
|
| 315 |
+
threads=args.threads,
|
| 316 |
+
ready_file=args.ready_file,
|
| 317 |
+
ops_class=fuser,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
if __name__ == "__main__":
|
| 322 |
+
import sys
|
| 323 |
+
|
| 324 |
+
main(sys.argv[1:])
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/generic.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import inspect
|
| 2 |
+
|
| 3 |
+
from .asyn import AsyncFileSystem
|
| 4 |
+
from .callbacks import _DEFAULT_CALLBACK
|
| 5 |
+
from .core import filesystem, get_filesystem_class, split_protocol
|
| 6 |
+
|
| 7 |
+
_generic_fs = {}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def set_generic_fs(protocol, **storage_options):
|
| 11 |
+
_generic_fs[protocol] = filesystem(protocol, **storage_options)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
default_method = "default"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _resolve_fs(url, method=None, protocol=None, storage_options=None):
|
| 18 |
+
"""Pick instance of backend FS"""
|
| 19 |
+
method = method or default_method
|
| 20 |
+
protocol = protocol or split_protocol(url)[0]
|
| 21 |
+
storage_options = storage_options or {}
|
| 22 |
+
if method == "default":
|
| 23 |
+
return filesystem(protocol)
|
| 24 |
+
if method == "generic":
|
| 25 |
+
return _generic_fs[protocol]
|
| 26 |
+
if method == "current":
|
| 27 |
+
cls = get_filesystem_class(protocol)
|
| 28 |
+
return cls.current()
|
| 29 |
+
if method == "options":
|
| 30 |
+
return filesystem(protocol, **storage_options.get(protocol, {}))
|
| 31 |
+
raise ValueError(f"Unknown FS resolution method: {method}")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class GenericFileSystem(AsyncFileSystem):
|
| 35 |
+
"""Wrapper over all other FS types
|
| 36 |
+
|
| 37 |
+
<experimental!>
|
| 38 |
+
|
| 39 |
+
This implementation is a single unified interface to be able to run FS operations
|
| 40 |
+
over generic URLs, and dispatch to the specific implementations using the URL
|
| 41 |
+
protocol prefix.
|
| 42 |
+
|
| 43 |
+
Note: instances of this FS are always async, even if you never use it with any async
|
| 44 |
+
backend.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
protocol = "generic" # there is no real reason to ever use a protocol with this FS
|
| 48 |
+
|
| 49 |
+
def __init__(self, default_method=None, **kwargs):
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
Parameters
|
| 53 |
+
----------
|
| 54 |
+
default_method: str (optional)
|
| 55 |
+
Defines how to configure backend FS instances. Options are:
|
| 56 |
+
- "default" (you get this with None): instantiate like FSClass(), with no
|
| 57 |
+
extra arguments; this is the default instance of that FS, and can be
|
| 58 |
+
configured via the config system
|
| 59 |
+
- "generic": takes instances from the `_generic_fs` dict in this module,
|
| 60 |
+
which you must populate before use. Keys are by protocol
|
| 61 |
+
- "current": takes the most recently instantiated version of each FS
|
| 62 |
+
- "options": expect ``storage_options`` to be passed along with every call.
|
| 63 |
+
"""
|
| 64 |
+
self.method = default_method
|
| 65 |
+
super(GenericFileSystem, self).__init__(**kwargs)
|
| 66 |
+
|
| 67 |
+
async def _info(
|
| 68 |
+
self, url, method=None, protocol=None, storage_options=None, fs=None, **kwargs
|
| 69 |
+
):
|
| 70 |
+
fs = fs or _resolve_fs(url, method or self.method, protocol, storage_options)
|
| 71 |
+
if fs.async_impl:
|
| 72 |
+
out = await fs._info(url, **kwargs)
|
| 73 |
+
else:
|
| 74 |
+
out = fs.info(url, **kwargs)
|
| 75 |
+
out["name"] = fs.unstrip_protocol(out["name"])
|
| 76 |
+
return out
|
| 77 |
+
|
| 78 |
+
async def _ls(
|
| 79 |
+
self,
|
| 80 |
+
url,
|
| 81 |
+
method=None,
|
| 82 |
+
protocol=None,
|
| 83 |
+
storage_options=None,
|
| 84 |
+
fs=None,
|
| 85 |
+
detail=True,
|
| 86 |
+
**kwargs,
|
| 87 |
+
):
|
| 88 |
+
fs = fs or _resolve_fs(url, method or self.method, protocol, storage_options)
|
| 89 |
+
if fs.async_impl:
|
| 90 |
+
out = await fs._ls(url, detail=True, **kwargs)
|
| 91 |
+
else:
|
| 92 |
+
out = fs.ls(url, detail=True, **kwargs)
|
| 93 |
+
for o in out:
|
| 94 |
+
o["name"] = fs.unstrip_protocol(o["name"])
|
| 95 |
+
if detail:
|
| 96 |
+
return out
|
| 97 |
+
else:
|
| 98 |
+
return [o["name"] for o in out]
|
| 99 |
+
|
| 100 |
+
async def _rm(
|
| 101 |
+
self, url, method=None, protocol=None, storage_options=None, fs=None, **kwargs
|
| 102 |
+
):
|
| 103 |
+
fs = fs or _resolve_fs(url, method or self.method, protocol, storage_options)
|
| 104 |
+
if fs.async_impl:
|
| 105 |
+
await fs._rm(url, **kwargs)
|
| 106 |
+
else:
|
| 107 |
+
fs.rm(url, **kwargs)
|
| 108 |
+
|
| 109 |
+
async def _cp_file(
|
| 110 |
+
self,
|
| 111 |
+
url,
|
| 112 |
+
url2,
|
| 113 |
+
method=None,
|
| 114 |
+
protocol=None,
|
| 115 |
+
storage_options=None,
|
| 116 |
+
fs=None,
|
| 117 |
+
method2=None,
|
| 118 |
+
protocol2=None,
|
| 119 |
+
storage_options2=None,
|
| 120 |
+
fs2=None,
|
| 121 |
+
blocksize=2**20,
|
| 122 |
+
callback=_DEFAULT_CALLBACK,
|
| 123 |
+
**kwargs,
|
| 124 |
+
):
|
| 125 |
+
fs = fs or _resolve_fs(url, method or self.method, protocol, storage_options)
|
| 126 |
+
fs2 = fs2 or _resolve_fs(
|
| 127 |
+
url2, method2 or self.method, protocol2, storage_options2
|
| 128 |
+
)
|
| 129 |
+
if fs is fs2:
|
| 130 |
+
# pure remote
|
| 131 |
+
if fs.async_impl:
|
| 132 |
+
return await fs._cp_file(url, url2, **kwargs)
|
| 133 |
+
else:
|
| 134 |
+
return fs.cp_file(url, url2, **kwargs)
|
| 135 |
+
kw = {"blocksize": 0, "cache_type": "none"}
|
| 136 |
+
try:
|
| 137 |
+
f1 = (
|
| 138 |
+
await fs.open_async(url, "rb")
|
| 139 |
+
if hasattr(fs, "open_async")
|
| 140 |
+
else fs.open(url, "rb", **kw)
|
| 141 |
+
)
|
| 142 |
+
callback.set_size(maybe_await(f1.size))
|
| 143 |
+
f2 = (
|
| 144 |
+
await fs2.open_async(url2, "wb")
|
| 145 |
+
if hasattr(fs2, "open_async")
|
| 146 |
+
else fs2.open(url2, "wb", **kw)
|
| 147 |
+
)
|
| 148 |
+
while f1.size is None or f2.tell() < f1.size:
|
| 149 |
+
data = await maybe_await(f1.read(blocksize))
|
| 150 |
+
if f1.size is None and not data:
|
| 151 |
+
break
|
| 152 |
+
await maybe_await(f2.write(data))
|
| 153 |
+
callback.absolute_update(f2.tell())
|
| 154 |
+
finally:
|
| 155 |
+
try:
|
| 156 |
+
await maybe_await(f2.close())
|
| 157 |
+
await maybe_await(f1.close())
|
| 158 |
+
except NameError:
|
| 159 |
+
# fail while opening f1 or f2
|
| 160 |
+
pass
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
async def maybe_await(cor):
|
| 164 |
+
if inspect.iscoroutine(cor):
|
| 165 |
+
return await cor
|
| 166 |
+
else:
|
| 167 |
+
return cor
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/gui.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
import contextlib
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
|
| 7 |
+
import panel as pn
|
| 8 |
+
|
| 9 |
+
from .core import OpenFile, get_filesystem_class, split_protocol
|
| 10 |
+
from .registry import known_implementations
|
| 11 |
+
|
| 12 |
+
pn.extension()
|
| 13 |
+
logger = logging.getLogger("fsspec.gui")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SigSlot(object):
|
| 17 |
+
"""Signal-slot mixin, for Panel event passing
|
| 18 |
+
|
| 19 |
+
Include this class in a widget manager's superclasses to be able to
|
| 20 |
+
register events and callbacks on Panel widgets managed by that class.
|
| 21 |
+
|
| 22 |
+
The method ``_register`` should be called as widgets are added, and external
|
| 23 |
+
code should call ``connect`` to associate callbacks.
|
| 24 |
+
|
| 25 |
+
By default, all signals emit a DEBUG logging statement.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
signals = [] # names of signals that this class may emit
|
| 29 |
+
# each of which must be set by _register for any new instance
|
| 30 |
+
slots = [] # names of actions that this class may respond to
|
| 31 |
+
|
| 32 |
+
# each of which must be a method name
|
| 33 |
+
|
| 34 |
+
def __init__(self):
|
| 35 |
+
self._ignoring_events = False
|
| 36 |
+
self._sigs = {}
|
| 37 |
+
self._map = {}
|
| 38 |
+
self._setup()
|
| 39 |
+
|
| 40 |
+
def _setup(self):
|
| 41 |
+
"""Create GUI elements and register signals"""
|
| 42 |
+
self.panel = pn.pane.PaneBase()
|
| 43 |
+
# no signals to set up in the base class
|
| 44 |
+
|
| 45 |
+
def _register(
|
| 46 |
+
self, widget, name, thing="value", log_level=logging.DEBUG, auto=False
|
| 47 |
+
):
|
| 48 |
+
"""Watch the given attribute of a widget and assign it a named event
|
| 49 |
+
|
| 50 |
+
This is normally called at the time a widget is instantiated, in the
|
| 51 |
+
class which owns it.
|
| 52 |
+
|
| 53 |
+
Parameters
|
| 54 |
+
----------
|
| 55 |
+
widget : pn.layout.Panel or None
|
| 56 |
+
Widget to watch. If None, an anonymous signal not associated with
|
| 57 |
+
any widget.
|
| 58 |
+
name : str
|
| 59 |
+
Name of this event
|
| 60 |
+
thing : str
|
| 61 |
+
Attribute of the given widget to watch
|
| 62 |
+
log_level : int
|
| 63 |
+
When the signal is triggered, a logging event of the given level
|
| 64 |
+
will be fired in the dfviz logger.
|
| 65 |
+
auto : bool
|
| 66 |
+
If True, automatically connects with a method in this class of the
|
| 67 |
+
same name.
|
| 68 |
+
"""
|
| 69 |
+
if name not in self.signals:
|
| 70 |
+
raise ValueError("Attempt to assign an undeclared signal: %s" % name)
|
| 71 |
+
self._sigs[name] = {
|
| 72 |
+
"widget": widget,
|
| 73 |
+
"callbacks": [],
|
| 74 |
+
"thing": thing,
|
| 75 |
+
"log": log_level,
|
| 76 |
+
}
|
| 77 |
+
wn = "-".join(
|
| 78 |
+
[
|
| 79 |
+
getattr(widget, "name", str(widget)) if widget is not None else "none",
|
| 80 |
+
thing,
|
| 81 |
+
]
|
| 82 |
+
)
|
| 83 |
+
self._map[wn] = name
|
| 84 |
+
if widget is not None:
|
| 85 |
+
widget.param.watch(self._signal, thing, onlychanged=True)
|
| 86 |
+
if auto and hasattr(self, name):
|
| 87 |
+
self.connect(name, getattr(self, name))
|
| 88 |
+
|
| 89 |
+
def _repr_mimebundle_(self, *args, **kwargs):
|
| 90 |
+
"""Display in a notebook or a server"""
|
| 91 |
+
try:
|
| 92 |
+
return self.panel._repr_mimebundle_(*args, **kwargs)
|
| 93 |
+
except (ValueError, AttributeError):
|
| 94 |
+
raise NotImplementedError("Panel does not seem to be set " "up properly")
|
| 95 |
+
|
| 96 |
+
def connect(self, signal, slot):
|
| 97 |
+
"""Associate call back with given event
|
| 98 |
+
|
| 99 |
+
The callback must be a function which takes the "new" value of the
|
| 100 |
+
watched attribute as the only parameter. If the callback return False,
|
| 101 |
+
this cancels any further processing of the given event.
|
| 102 |
+
|
| 103 |
+
Alternatively, the callback can be a string, in which case it means
|
| 104 |
+
emitting the correspondingly-named event (i.e., connect to self)
|
| 105 |
+
"""
|
| 106 |
+
self._sigs[signal]["callbacks"].append(slot)
|
| 107 |
+
|
| 108 |
+
def _signal(self, event):
|
| 109 |
+
"""This is called by a an action on a widget
|
| 110 |
+
|
| 111 |
+
Within an self.ignore_events context, nothing happens.
|
| 112 |
+
|
| 113 |
+
Tests can execute this method by directly changing the values of
|
| 114 |
+
widget components.
|
| 115 |
+
"""
|
| 116 |
+
if not self._ignoring_events:
|
| 117 |
+
wn = "-".join([event.obj.name, event.name])
|
| 118 |
+
if wn in self._map and self._map[wn] in self._sigs:
|
| 119 |
+
self._emit(self._map[wn], event.new)
|
| 120 |
+
|
| 121 |
+
@contextlib.contextmanager
|
| 122 |
+
def ignore_events(self):
|
| 123 |
+
"""Temporarily turn off events processing in this instance
|
| 124 |
+
|
| 125 |
+
(does not propagate to children)
|
| 126 |
+
"""
|
| 127 |
+
self._ignoring_events = True
|
| 128 |
+
try:
|
| 129 |
+
yield
|
| 130 |
+
finally:
|
| 131 |
+
self._ignoring_events = False
|
| 132 |
+
|
| 133 |
+
def _emit(self, sig, value=None):
|
| 134 |
+
"""An event happened, call its callbacks
|
| 135 |
+
|
| 136 |
+
This method can be used in tests to simulate message passing without
|
| 137 |
+
directly changing visual elements.
|
| 138 |
+
|
| 139 |
+
Calling of callbacks will halt whenever one returns False.
|
| 140 |
+
"""
|
| 141 |
+
logger.log(self._sigs[sig]["log"], "{}: {}".format(sig, value))
|
| 142 |
+
for callback in self._sigs[sig]["callbacks"]:
|
| 143 |
+
if isinstance(callback, str):
|
| 144 |
+
self._emit(callback)
|
| 145 |
+
else:
|
| 146 |
+
try:
|
| 147 |
+
# running callbacks should not break the interface
|
| 148 |
+
ret = callback(value)
|
| 149 |
+
if ret is False:
|
| 150 |
+
break
|
| 151 |
+
except Exception as e:
|
| 152 |
+
logger.exception(
|
| 153 |
+
"Exception (%s) while executing callback for signal: %s"
|
| 154 |
+
"" % (e, sig)
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
def show(self, threads=False):
|
| 158 |
+
"""Open a new browser tab and display this instance's interface"""
|
| 159 |
+
self.panel.show(threads=threads, verbose=False)
|
| 160 |
+
return self
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class SingleSelect(SigSlot):
|
| 164 |
+
"""A multiselect which only allows you to select one item for an event"""
|
| 165 |
+
|
| 166 |
+
signals = ["_selected", "selected"] # the first is internal
|
| 167 |
+
slots = ["set_options", "set_selection", "add", "clear", "select"]
|
| 168 |
+
|
| 169 |
+
def __init__(self, **kwargs):
|
| 170 |
+
self.kwargs = kwargs
|
| 171 |
+
super().__init__()
|
| 172 |
+
|
| 173 |
+
def _setup(self):
|
| 174 |
+
self.panel = pn.widgets.MultiSelect(**self.kwargs)
|
| 175 |
+
self._register(self.panel, "_selected", "value")
|
| 176 |
+
self._register(None, "selected")
|
| 177 |
+
self.connect("_selected", self.select_one)
|
| 178 |
+
|
| 179 |
+
def _signal(self, *args, **kwargs):
|
| 180 |
+
super()._signal(*args, **kwargs)
|
| 181 |
+
|
| 182 |
+
def select_one(self, *_):
|
| 183 |
+
with self.ignore_events():
|
| 184 |
+
val = [self.panel.value[-1]] if self.panel.value else []
|
| 185 |
+
self.panel.value = val
|
| 186 |
+
self._emit("selected", self.panel.value)
|
| 187 |
+
|
| 188 |
+
def set_options(self, options):
|
| 189 |
+
self.panel.options = options
|
| 190 |
+
|
| 191 |
+
def clear(self):
|
| 192 |
+
self.panel.options = []
|
| 193 |
+
|
| 194 |
+
@property
|
| 195 |
+
def value(self):
|
| 196 |
+
return self.panel.value
|
| 197 |
+
|
| 198 |
+
def set_selection(self, selection):
|
| 199 |
+
self.panel.value = [selection]
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class FileSelector(SigSlot):
|
| 203 |
+
"""Panel-based graphical file selector widget
|
| 204 |
+
|
| 205 |
+
Instances of this widget are interactive and can be displayed in jupyter by having
|
| 206 |
+
them as the output of a cell, or in a separate browser tab using ``.show()``.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
signals = [
|
| 210 |
+
"protocol_changed",
|
| 211 |
+
"selection_changed",
|
| 212 |
+
"directory_entered",
|
| 213 |
+
"home_clicked",
|
| 214 |
+
"up_clicked",
|
| 215 |
+
"go_clicked",
|
| 216 |
+
"filters_changed",
|
| 217 |
+
]
|
| 218 |
+
slots = ["set_filters", "go_home"]
|
| 219 |
+
|
| 220 |
+
def __init__(self, url=None, filters=None, ignore=None, kwargs=None):
|
| 221 |
+
"""
|
| 222 |
+
|
| 223 |
+
Parameters
|
| 224 |
+
----------
|
| 225 |
+
url : str (optional)
|
| 226 |
+
Initial value of the URL to populate the dialog; should include protocol
|
| 227 |
+
filters : list(str) (optional)
|
| 228 |
+
File endings to include in the listings. If not included, all files are
|
| 229 |
+
allowed. Does not affect directories.
|
| 230 |
+
If given, the endings will appear as checkboxes in the interface
|
| 231 |
+
ignore : list(str) (optional)
|
| 232 |
+
Regex(s) of file basename patterns to ignore, e.g., "\\." for typical
|
| 233 |
+
hidden files on posix
|
| 234 |
+
kwargs : dict (optional)
|
| 235 |
+
To pass to file system instance
|
| 236 |
+
"""
|
| 237 |
+
if url:
|
| 238 |
+
self.init_protocol, url = split_protocol(url)
|
| 239 |
+
else:
|
| 240 |
+
self.init_protocol, url = "file", os.getcwd()
|
| 241 |
+
self.init_url = url
|
| 242 |
+
self.init_kwargs = kwargs or "{}"
|
| 243 |
+
self.filters = filters
|
| 244 |
+
self.ignore = [re.compile(i) for i in ignore or []]
|
| 245 |
+
self._fs = None
|
| 246 |
+
super().__init__()
|
| 247 |
+
|
| 248 |
+
def _setup(self):
|
| 249 |
+
self.url = pn.widgets.TextInput(
|
| 250 |
+
name="url",
|
| 251 |
+
value=self.init_url,
|
| 252 |
+
align="end",
|
| 253 |
+
sizing_mode="stretch_width",
|
| 254 |
+
width_policy="max",
|
| 255 |
+
)
|
| 256 |
+
self.protocol = pn.widgets.Select(
|
| 257 |
+
options=list(sorted(known_implementations)),
|
| 258 |
+
value=self.init_protocol,
|
| 259 |
+
name="protocol",
|
| 260 |
+
align="center",
|
| 261 |
+
)
|
| 262 |
+
self.kwargs = pn.widgets.TextInput(name="kwargs", value="{}", align="center")
|
| 263 |
+
self.go = pn.widgets.Button(name="⇨", align="end", width=45)
|
| 264 |
+
self.main = SingleSelect(size=10)
|
| 265 |
+
self.home = pn.widgets.Button(name="🏠", width=40, height=30, align="end")
|
| 266 |
+
self.up = pn.widgets.Button(name="‹", width=30, height=30, align="end")
|
| 267 |
+
|
| 268 |
+
self._register(self.protocol, "protocol_changed", auto=True)
|
| 269 |
+
self._register(self.go, "go_clicked", "clicks", auto=True)
|
| 270 |
+
self._register(self.up, "up_clicked", "clicks", auto=True)
|
| 271 |
+
self._register(self.home, "home_clicked", "clicks", auto=True)
|
| 272 |
+
self._register(None, "selection_changed")
|
| 273 |
+
self.main.connect("selected", self.selection_changed)
|
| 274 |
+
self._register(None, "directory_entered")
|
| 275 |
+
self.prev_protocol = self.protocol.value
|
| 276 |
+
self.prev_kwargs = self.storage_options
|
| 277 |
+
|
| 278 |
+
self.filter_sel = pn.widgets.CheckBoxGroup(
|
| 279 |
+
value=[], options=[], inline=False, align="end", width_policy="min"
|
| 280 |
+
)
|
| 281 |
+
self._register(self.filter_sel, "filters_changed", auto=True)
|
| 282 |
+
|
| 283 |
+
self.panel = pn.Column(
|
| 284 |
+
pn.Row(self.protocol, self.kwargs),
|
| 285 |
+
pn.Row(self.home, self.up, self.url, self.go, self.filter_sel),
|
| 286 |
+
self.main.panel,
|
| 287 |
+
)
|
| 288 |
+
self.set_filters(self.filters)
|
| 289 |
+
self.go_clicked()
|
| 290 |
+
|
| 291 |
+
def set_filters(self, filters=None):
|
| 292 |
+
self.filters = filters
|
| 293 |
+
if filters:
|
| 294 |
+
self.filter_sel.options = filters
|
| 295 |
+
self.filter_sel.value = filters
|
| 296 |
+
else:
|
| 297 |
+
self.filter_sel.options = []
|
| 298 |
+
self.filter_sel.value = []
|
| 299 |
+
|
| 300 |
+
@property
|
| 301 |
+
def storage_options(self):
|
| 302 |
+
"""Value of the kwargs box as a dictionary"""
|
| 303 |
+
return ast.literal_eval(self.kwargs.value) or {}
|
| 304 |
+
|
| 305 |
+
@property
|
| 306 |
+
def fs(self):
|
| 307 |
+
"""Current filesystem instance"""
|
| 308 |
+
if self._fs is None:
|
| 309 |
+
cls = get_filesystem_class(self.protocol.value)
|
| 310 |
+
self._fs = cls(**self.storage_options)
|
| 311 |
+
return self._fs
|
| 312 |
+
|
| 313 |
+
@property
|
| 314 |
+
def urlpath(self):
|
| 315 |
+
"""URL of currently selected item"""
|
| 316 |
+
return (
|
| 317 |
+
(self.protocol.value + "://" + self.main.value[0])
|
| 318 |
+
if self.main.value
|
| 319 |
+
else None
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
def open_file(self, mode="rb", compression=None, encoding=None):
|
| 323 |
+
"""Create OpenFile instance for the currently selected item
|
| 324 |
+
|
| 325 |
+
For example, in a notebook you might do something like
|
| 326 |
+
|
| 327 |
+
.. code-block::
|
| 328 |
+
|
| 329 |
+
[ ]: sel = FileSelector(); sel
|
| 330 |
+
|
| 331 |
+
# user selects their file
|
| 332 |
+
|
| 333 |
+
[ ]: with sel.open_file('rb') as f:
|
| 334 |
+
... out = f.read()
|
| 335 |
+
|
| 336 |
+
Parameters
|
| 337 |
+
----------
|
| 338 |
+
mode: str (optional)
|
| 339 |
+
Open mode for the file.
|
| 340 |
+
compression: str (optional)
|
| 341 |
+
The interact with the file as compressed. Set to 'infer' to guess
|
| 342 |
+
compression from the file ending
|
| 343 |
+
encoding: str (optional)
|
| 344 |
+
If using text mode, use this encoding; defaults to UTF8.
|
| 345 |
+
"""
|
| 346 |
+
if self.urlpath is None:
|
| 347 |
+
raise ValueError("No file selected")
|
| 348 |
+
return OpenFile(self.fs, self.urlpath, mode, compression, encoding)
|
| 349 |
+
|
| 350 |
+
def filters_changed(self, values):
|
| 351 |
+
self.filters = values
|
| 352 |
+
self.go_clicked()
|
| 353 |
+
|
| 354 |
+
def selection_changed(self, *_):
|
| 355 |
+
if self.urlpath is None:
|
| 356 |
+
return
|
| 357 |
+
if self.fs.isdir(self.urlpath):
|
| 358 |
+
self.url.value = self.fs._strip_protocol(self.urlpath)
|
| 359 |
+
self.go_clicked()
|
| 360 |
+
|
| 361 |
+
def go_clicked(self, *_):
|
| 362 |
+
if (
|
| 363 |
+
self.prev_protocol != self.protocol.value
|
| 364 |
+
or self.prev_kwargs != self.storage_options
|
| 365 |
+
):
|
| 366 |
+
self._fs = None # causes fs to be recreated
|
| 367 |
+
self.prev_protocol = self.protocol.value
|
| 368 |
+
self.prev_kwargs = self.storage_options
|
| 369 |
+
listing = sorted(
|
| 370 |
+
self.fs.ls(self.url.value, detail=True), key=lambda x: x["name"]
|
| 371 |
+
)
|
| 372 |
+
listing = [
|
| 373 |
+
l
|
| 374 |
+
for l in listing
|
| 375 |
+
if not any(i.match(l["name"].rsplit("/", 1)[-1]) for i in self.ignore)
|
| 376 |
+
]
|
| 377 |
+
folders = {
|
| 378 |
+
"📁 " + o["name"].rsplit("/", 1)[-1]: o["name"]
|
| 379 |
+
for o in listing
|
| 380 |
+
if o["type"] == "directory"
|
| 381 |
+
}
|
| 382 |
+
files = {
|
| 383 |
+
"📄 " + o["name"].rsplit("/", 1)[-1]: o["name"]
|
| 384 |
+
for o in listing
|
| 385 |
+
if o["type"] == "file"
|
| 386 |
+
}
|
| 387 |
+
if self.filters:
|
| 388 |
+
files = {
|
| 389 |
+
k: v
|
| 390 |
+
for k, v in files.items()
|
| 391 |
+
if any(v.endswith(ext) for ext in self.filters)
|
| 392 |
+
}
|
| 393 |
+
self.main.set_options(dict(**folders, **files))
|
| 394 |
+
|
| 395 |
+
def protocol_changed(self, *_):
|
| 396 |
+
self._fs = None
|
| 397 |
+
self.main.options = []
|
| 398 |
+
self.url.value = ""
|
| 399 |
+
|
| 400 |
+
def home_clicked(self, *_):
|
| 401 |
+
self.protocol.value = self.init_protocol
|
| 402 |
+
self.kwargs.value = self.init_kwargs
|
| 403 |
+
self.url.value = self.init_url
|
| 404 |
+
self.go_clicked()
|
| 405 |
+
|
| 406 |
+
def up_clicked(self, *_):
|
| 407 |
+
self.url.value = self.fs._parent(self.url.value)
|
| 408 |
+
self.go_clicked()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/mapping.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import array
|
| 2 |
+
from collections.abc import MutableMapping
|
| 3 |
+
|
| 4 |
+
from .core import url_to_fs
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class FSMap(MutableMapping):
|
| 8 |
+
"""Wrap a FileSystem instance as a mutable wrapping.
|
| 9 |
+
|
| 10 |
+
The keys of the mapping become files under the given root, and the
|
| 11 |
+
values (which must be bytes) the contents of those files.
|
| 12 |
+
|
| 13 |
+
Parameters
|
| 14 |
+
----------
|
| 15 |
+
root: string
|
| 16 |
+
prefix for all the files
|
| 17 |
+
fs: FileSystem instance
|
| 18 |
+
check: bool (=True)
|
| 19 |
+
performs a touch at the location, to check for write access.
|
| 20 |
+
|
| 21 |
+
Examples
|
| 22 |
+
--------
|
| 23 |
+
>>> fs = FileSystem(**parameters) # doctest: +SKIP
|
| 24 |
+
>>> d = FSMap('my-data/path/', fs) # doctest: +SKIP
|
| 25 |
+
or, more likely
|
| 26 |
+
>>> d = fs.get_mapper('my-data/path/')
|
| 27 |
+
|
| 28 |
+
>>> d['loc1'] = b'Hello World' # doctest: +SKIP
|
| 29 |
+
>>> list(d.keys()) # doctest: +SKIP
|
| 30 |
+
['loc1']
|
| 31 |
+
>>> d['loc1'] # doctest: +SKIP
|
| 32 |
+
b'Hello World'
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(self, root, fs, check=False, create=False, missing_exceptions=None):
|
| 36 |
+
self.fs = fs
|
| 37 |
+
self.root = fs._strip_protocol(root).rstrip(
|
| 38 |
+
"/"
|
| 39 |
+
) # we join on '/' in _key_to_str
|
| 40 |
+
if missing_exceptions is None:
|
| 41 |
+
missing_exceptions = (
|
| 42 |
+
FileNotFoundError,
|
| 43 |
+
IsADirectoryError,
|
| 44 |
+
NotADirectoryError,
|
| 45 |
+
)
|
| 46 |
+
self.missing_exceptions = missing_exceptions
|
| 47 |
+
self.check = check
|
| 48 |
+
self.create = create
|
| 49 |
+
if create:
|
| 50 |
+
if not self.fs.exists(root):
|
| 51 |
+
self.fs.mkdir(root)
|
| 52 |
+
if check:
|
| 53 |
+
if not self.fs.exists(root):
|
| 54 |
+
raise ValueError(
|
| 55 |
+
"Path %s does not exist. Create "
|
| 56 |
+
" with the ``create=True`` keyword" % root
|
| 57 |
+
)
|
| 58 |
+
self.fs.touch(root + "/a")
|
| 59 |
+
self.fs.rm(root + "/a")
|
| 60 |
+
|
| 61 |
+
def clear(self):
|
| 62 |
+
"""Remove all keys below root - empties out mapping"""
|
| 63 |
+
try:
|
| 64 |
+
self.fs.rm(self.root, True)
|
| 65 |
+
self.fs.mkdir(self.root)
|
| 66 |
+
except: # noqa: E722
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
def getitems(self, keys, on_error="raise"):
|
| 70 |
+
"""Fetch multiple items from the store
|
| 71 |
+
|
| 72 |
+
If the backend is async-able, this might proceed concurrently
|
| 73 |
+
|
| 74 |
+
Parameters
|
| 75 |
+
----------
|
| 76 |
+
keys: list(str)
|
| 77 |
+
They keys to be fetched
|
| 78 |
+
on_error : "raise", "omit", "return"
|
| 79 |
+
If raise, an underlying exception will be raised (converted to KeyError
|
| 80 |
+
if the type is in self.missing_exceptions); if omit, keys with exception
|
| 81 |
+
will simply not be included in the output; if "return", all keys are
|
| 82 |
+
included in the output, but the value will be bytes or an exception
|
| 83 |
+
instance.
|
| 84 |
+
|
| 85 |
+
Returns
|
| 86 |
+
-------
|
| 87 |
+
dict(key, bytes|exception)
|
| 88 |
+
"""
|
| 89 |
+
keys2 = [self._key_to_str(k) for k in keys]
|
| 90 |
+
oe = on_error if on_error == "raise" else "return"
|
| 91 |
+
try:
|
| 92 |
+
out = self.fs.cat(keys2, on_error=oe)
|
| 93 |
+
if isinstance(out, bytes):
|
| 94 |
+
out = {keys2[0]: out}
|
| 95 |
+
except self.missing_exceptions as e:
|
| 96 |
+
raise KeyError from e
|
| 97 |
+
out = {
|
| 98 |
+
k: (KeyError() if isinstance(v, self.missing_exceptions) else v)
|
| 99 |
+
for k, v in out.items()
|
| 100 |
+
}
|
| 101 |
+
return {
|
| 102 |
+
key: out[k2]
|
| 103 |
+
for key, k2 in zip(keys, keys2)
|
| 104 |
+
if on_error == "return" or not isinstance(out[k2], BaseException)
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
def setitems(self, values_dict):
|
| 108 |
+
"""Set the values of multiple items in the store
|
| 109 |
+
|
| 110 |
+
Parameters
|
| 111 |
+
----------
|
| 112 |
+
values_dict: dict(str, bytes)
|
| 113 |
+
"""
|
| 114 |
+
values = {self._key_to_str(k): maybe_convert(v) for k, v in values_dict.items()}
|
| 115 |
+
self.fs.pipe(values)
|
| 116 |
+
|
| 117 |
+
def delitems(self, keys):
|
| 118 |
+
"""Remove multiple keys from the store"""
|
| 119 |
+
self.fs.rm([self._key_to_str(k) for k in keys])
|
| 120 |
+
|
| 121 |
+
def _key_to_str(self, key):
|
| 122 |
+
"""Generate full path for the key"""
|
| 123 |
+
if isinstance(key, (tuple, list)):
|
| 124 |
+
key = str(tuple(key))
|
| 125 |
+
else:
|
| 126 |
+
key = str(key)
|
| 127 |
+
return self.fs._strip_protocol("/".join([self.root, key]) if self.root else key)
|
| 128 |
+
|
| 129 |
+
def _str_to_key(self, s):
|
| 130 |
+
"""Strip path of to leave key name"""
|
| 131 |
+
return s[len(self.root) :].lstrip("/")
|
| 132 |
+
|
| 133 |
+
def __getitem__(self, key, default=None):
|
| 134 |
+
"""Retrieve data"""
|
| 135 |
+
k = self._key_to_str(key)
|
| 136 |
+
try:
|
| 137 |
+
result = self.fs.cat(k)
|
| 138 |
+
except self.missing_exceptions:
|
| 139 |
+
if default is not None:
|
| 140 |
+
return default
|
| 141 |
+
raise KeyError(key)
|
| 142 |
+
return result
|
| 143 |
+
|
| 144 |
+
def pop(self, key, default=None):
|
| 145 |
+
result = self.__getitem__(key, default)
|
| 146 |
+
try:
|
| 147 |
+
del self[key]
|
| 148 |
+
except KeyError:
|
| 149 |
+
pass
|
| 150 |
+
return result
|
| 151 |
+
|
| 152 |
+
def __setitem__(self, key, value):
|
| 153 |
+
"""Store value in key"""
|
| 154 |
+
key = self._key_to_str(key)
|
| 155 |
+
self.fs.mkdirs(self.fs._parent(key), exist_ok=True)
|
| 156 |
+
self.fs.pipe_file(key, maybe_convert(value))
|
| 157 |
+
|
| 158 |
+
def __iter__(self):
|
| 159 |
+
return (self._str_to_key(x) for x in self.fs.find(self.root))
|
| 160 |
+
|
| 161 |
+
def __len__(self):
|
| 162 |
+
return len(self.fs.find(self.root))
|
| 163 |
+
|
| 164 |
+
def __delitem__(self, key):
|
| 165 |
+
"""Remove key"""
|
| 166 |
+
try:
|
| 167 |
+
self.fs.rm(self._key_to_str(key))
|
| 168 |
+
except: # noqa: E722
|
| 169 |
+
raise KeyError
|
| 170 |
+
|
| 171 |
+
def __contains__(self, key):
|
| 172 |
+
"""Does key exist in mapping?"""
|
| 173 |
+
path = self._key_to_str(key)
|
| 174 |
+
return self.fs.exists(path) and self.fs.isfile(path)
|
| 175 |
+
|
| 176 |
+
def __reduce__(self):
|
| 177 |
+
return FSMap, (self.root, self.fs, False, False, self.missing_exceptions)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def maybe_convert(value):
|
| 181 |
+
if isinstance(value, array.array) or hasattr(value, "__array__"):
|
| 182 |
+
# bytes-like things
|
| 183 |
+
if hasattr(value, "dtype") and value.dtype.kind in "Mm":
|
| 184 |
+
# The buffer interface doesn't support datetime64/timdelta64 numpy
|
| 185 |
+
# arrays
|
| 186 |
+
value = value.view("int64")
|
| 187 |
+
value = bytearray(memoryview(value))
|
| 188 |
+
return value
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def get_mapper(
|
| 192 |
+
url="",
|
| 193 |
+
check=False,
|
| 194 |
+
create=False,
|
| 195 |
+
missing_exceptions=None,
|
| 196 |
+
alternate_root=None,
|
| 197 |
+
**kwargs,
|
| 198 |
+
):
|
| 199 |
+
"""Create key-value interface for given URL and options
|
| 200 |
+
|
| 201 |
+
The URL will be of the form "protocol://location" and point to the root
|
| 202 |
+
of the mapper required. All keys will be file-names below this location,
|
| 203 |
+
and their values the contents of each key.
|
| 204 |
+
|
| 205 |
+
Also accepts compound URLs like zip::s3://bucket/file.zip , see ``fsspec.open``.
|
| 206 |
+
|
| 207 |
+
Parameters
|
| 208 |
+
----------
|
| 209 |
+
url: str
|
| 210 |
+
Root URL of mapping
|
| 211 |
+
check: bool
|
| 212 |
+
Whether to attempt to read from the location before instantiation, to
|
| 213 |
+
check that the mapping does exist
|
| 214 |
+
create: bool
|
| 215 |
+
Whether to make the directory corresponding to the root before
|
| 216 |
+
instantiating
|
| 217 |
+
missing_exceptions: None or tuple
|
| 218 |
+
If given, these exception types will be regarded as missing keys and
|
| 219 |
+
return KeyError when trying to read data. By default, you get
|
| 220 |
+
(FileNotFoundError, IsADirectoryError, NotADirectoryError)
|
| 221 |
+
alternate_root: None or str
|
| 222 |
+
In cases of complex URLs, the parser may fail to pick the correct part
|
| 223 |
+
for the mapper root, so this arg can override
|
| 224 |
+
|
| 225 |
+
Returns
|
| 226 |
+
-------
|
| 227 |
+
``FSMap`` instance, the dict-like key-value store.
|
| 228 |
+
"""
|
| 229 |
+
# Removing protocol here - could defer to each open() on the backend
|
| 230 |
+
fs, urlpath = url_to_fs(url, **kwargs)
|
| 231 |
+
root = alternate_root if alternate_root is not None else urlpath
|
| 232 |
+
return FSMap(root, fs, check, create, missing_exceptions=missing_exceptions)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/parquet.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import json
|
| 3 |
+
import warnings
|
| 4 |
+
|
| 5 |
+
from .core import url_to_fs
|
| 6 |
+
from .utils import merge_offset_ranges
|
| 7 |
+
|
| 8 |
+
# Parquet-Specific Utilities for fsspec
|
| 9 |
+
#
|
| 10 |
+
# Most of the functions defined in this module are NOT
|
| 11 |
+
# intended for public consumption. The only exception
|
| 12 |
+
# to this is `open_parquet_file`, which should be used
|
| 13 |
+
# place of `fs.open()` to open parquet-formatted files
|
| 14 |
+
# on remote file systems.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def open_parquet_file(
|
| 18 |
+
path,
|
| 19 |
+
mode="rb",
|
| 20 |
+
fs=None,
|
| 21 |
+
metadata=None,
|
| 22 |
+
columns=None,
|
| 23 |
+
row_groups=None,
|
| 24 |
+
storage_options=None,
|
| 25 |
+
strict=False,
|
| 26 |
+
engine="auto",
|
| 27 |
+
max_gap=64_000,
|
| 28 |
+
max_block=256_000_000,
|
| 29 |
+
footer_sample_size=1_000_000,
|
| 30 |
+
**kwargs,
|
| 31 |
+
):
|
| 32 |
+
"""
|
| 33 |
+
Return a file-like object for a single Parquet file.
|
| 34 |
+
|
| 35 |
+
The specified parquet `engine` will be used to parse the
|
| 36 |
+
footer metadata, and determine the required byte ranges
|
| 37 |
+
from the file. The target path will then be opened with
|
| 38 |
+
the "parts" (`KnownPartsOfAFile`) caching strategy.
|
| 39 |
+
|
| 40 |
+
Note that this method is intended for usage with remote
|
| 41 |
+
file systems, and is unlikely to improve parquet-read
|
| 42 |
+
performance on local file systems.
|
| 43 |
+
|
| 44 |
+
Parameters
|
| 45 |
+
----------
|
| 46 |
+
path: str
|
| 47 |
+
Target file path.
|
| 48 |
+
mode: str, optional
|
| 49 |
+
Mode option to be passed through to `fs.open`. Default is "rb".
|
| 50 |
+
metadata: Any, optional
|
| 51 |
+
Parquet metadata object. Object type must be supported
|
| 52 |
+
by the backend parquet engine. For now, only the "fastparquet"
|
| 53 |
+
engine supports an explicit `ParquetFile` metadata object.
|
| 54 |
+
If a metadata object is supplied, the remote footer metadata
|
| 55 |
+
will not need to be transferred into local memory.
|
| 56 |
+
fs: AbstractFileSystem, optional
|
| 57 |
+
Filesystem object to use for opening the file. If nothing is
|
| 58 |
+
specified, an `AbstractFileSystem` object will be inferred.
|
| 59 |
+
engine : str, default "auto"
|
| 60 |
+
Parquet engine to use for metadata parsing. Allowed options
|
| 61 |
+
include "fastparquet", "pyarrow", and "auto". The specified
|
| 62 |
+
engine must be installed in the current environment. If
|
| 63 |
+
"auto" is specified, and both engines are installed,
|
| 64 |
+
"fastparquet" will take precedence over "pyarrow".
|
| 65 |
+
columns: list, optional
|
| 66 |
+
List of all column names that may be read from the file.
|
| 67 |
+
row_groups : list, optional
|
| 68 |
+
List of all row-groups that may be read from the file. This
|
| 69 |
+
may be a list of row-group indices (integers), or it may be
|
| 70 |
+
a list of `RowGroup` metadata objects (if the "fastparquet"
|
| 71 |
+
engine is used).
|
| 72 |
+
storage_options : dict, optional
|
| 73 |
+
Used to generate an `AbstractFileSystem` object if `fs` was
|
| 74 |
+
not specified.
|
| 75 |
+
strict : bool, optional
|
| 76 |
+
Whether the resulting `KnownPartsOfAFile` cache should
|
| 77 |
+
fetch reads that go beyond a known byte-range boundary.
|
| 78 |
+
If `False` (the default), any read that ends outside a
|
| 79 |
+
known part will be zero padded. Note that using
|
| 80 |
+
`strict=True` may be useful for debugging.
|
| 81 |
+
max_gap : int, optional
|
| 82 |
+
Neighboring byte ranges will only be merged when their
|
| 83 |
+
inter-range gap is <= `max_gap`. Default is 64KB.
|
| 84 |
+
max_block : int, optional
|
| 85 |
+
Neighboring byte ranges will only be merged when the size of
|
| 86 |
+
the aggregated range is <= `max_block`. Default is 256MB.
|
| 87 |
+
footer_sample_size : int, optional
|
| 88 |
+
Number of bytes to read from the end of the path to look
|
| 89 |
+
for the footer metadata. If the sampled bytes do not contain
|
| 90 |
+
the footer, a second read request will be required, and
|
| 91 |
+
performance will suffer. Default is 1MB.
|
| 92 |
+
**kwargs :
|
| 93 |
+
Optional key-word arguments to pass to `fs.open`
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
# Make sure we have an `AbstractFileSystem` object
|
| 97 |
+
# to work with
|
| 98 |
+
if fs is None:
|
| 99 |
+
fs = url_to_fs(path, **(storage_options or {}))[0]
|
| 100 |
+
|
| 101 |
+
# For now, `columns == []` not supported. Just use
|
| 102 |
+
# default `open` command with `path` input
|
| 103 |
+
if columns is not None and len(columns) == 0:
|
| 104 |
+
return fs.open(path, mode=mode)
|
| 105 |
+
|
| 106 |
+
# Set the engine
|
| 107 |
+
engine = _set_engine(engine)
|
| 108 |
+
|
| 109 |
+
# Fetch the known byte ranges needed to read
|
| 110 |
+
# `columns` and/or `row_groups`
|
| 111 |
+
data = _get_parquet_byte_ranges(
|
| 112 |
+
[path],
|
| 113 |
+
fs,
|
| 114 |
+
metadata=metadata,
|
| 115 |
+
columns=columns,
|
| 116 |
+
row_groups=row_groups,
|
| 117 |
+
engine=engine,
|
| 118 |
+
max_gap=max_gap,
|
| 119 |
+
max_block=max_block,
|
| 120 |
+
footer_sample_size=footer_sample_size,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
# Extract file name from `data`
|
| 124 |
+
fn = next(iter(data)) if data else path
|
| 125 |
+
|
| 126 |
+
# Call self.open with "parts" caching
|
| 127 |
+
options = kwargs.pop("cache_options", {}).copy()
|
| 128 |
+
return fs.open(
|
| 129 |
+
fn,
|
| 130 |
+
mode=mode,
|
| 131 |
+
cache_type="parts",
|
| 132 |
+
cache_options={
|
| 133 |
+
**options,
|
| 134 |
+
**{
|
| 135 |
+
"data": data.get(fn, {}),
|
| 136 |
+
"strict": strict,
|
| 137 |
+
},
|
| 138 |
+
},
|
| 139 |
+
**kwargs,
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _get_parquet_byte_ranges(
|
| 144 |
+
paths,
|
| 145 |
+
fs,
|
| 146 |
+
metadata=None,
|
| 147 |
+
columns=None,
|
| 148 |
+
row_groups=None,
|
| 149 |
+
max_gap=64_000,
|
| 150 |
+
max_block=256_000_000,
|
| 151 |
+
footer_sample_size=1_000_000,
|
| 152 |
+
engine="auto",
|
| 153 |
+
):
|
| 154 |
+
"""Get a dictionary of the known byte ranges needed
|
| 155 |
+
to read a specific column/row-group selection from a
|
| 156 |
+
Parquet dataset. Each value in the output dictionary
|
| 157 |
+
is intended for use as the `data` argument for the
|
| 158 |
+
`KnownPartsOfAFile` caching strategy of a single path.
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
# Set engine if necessary
|
| 162 |
+
if isinstance(engine, str):
|
| 163 |
+
engine = _set_engine(engine)
|
| 164 |
+
|
| 165 |
+
# Pass to specialized function if metadata is defined
|
| 166 |
+
if metadata is not None:
|
| 167 |
+
|
| 168 |
+
# Use the provided parquet metadata object
|
| 169 |
+
# to avoid transferring/parsing footer metadata
|
| 170 |
+
return _get_parquet_byte_ranges_from_metadata(
|
| 171 |
+
metadata,
|
| 172 |
+
fs,
|
| 173 |
+
engine,
|
| 174 |
+
columns=columns,
|
| 175 |
+
row_groups=row_groups,
|
| 176 |
+
max_gap=max_gap,
|
| 177 |
+
max_block=max_block,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# Get file sizes asynchronously
|
| 181 |
+
file_sizes = fs.sizes(paths)
|
| 182 |
+
|
| 183 |
+
# Populate global paths, starts, & ends
|
| 184 |
+
result = {}
|
| 185 |
+
data_paths = []
|
| 186 |
+
data_starts = []
|
| 187 |
+
data_ends = []
|
| 188 |
+
add_header_magic = True
|
| 189 |
+
if columns is None and row_groups is None:
|
| 190 |
+
# We are NOT selecting specific columns or row-groups.
|
| 191 |
+
#
|
| 192 |
+
# We can avoid sampling the footers, and just transfer
|
| 193 |
+
# all file data with cat_ranges
|
| 194 |
+
for i, path in enumerate(paths):
|
| 195 |
+
result[path] = {}
|
| 196 |
+
for b in range(0, file_sizes[i], max_block):
|
| 197 |
+
data_paths.append(path)
|
| 198 |
+
data_starts.append(b)
|
| 199 |
+
data_ends.append(min(b + max_block, file_sizes[i]))
|
| 200 |
+
add_header_magic = False # "Magic" should already be included
|
| 201 |
+
else:
|
| 202 |
+
# We ARE selecting specific columns or row-groups.
|
| 203 |
+
#
|
| 204 |
+
# Gather file footers.
|
| 205 |
+
# We just take the last `footer_sample_size` bytes of each
|
| 206 |
+
# file (or the entire file if it is smaller than that)
|
| 207 |
+
footer_starts = []
|
| 208 |
+
footer_ends = []
|
| 209 |
+
for i, path in enumerate(paths):
|
| 210 |
+
footer_ends.append(file_sizes[i])
|
| 211 |
+
sample_size = max(0, file_sizes[i] - footer_sample_size)
|
| 212 |
+
footer_starts.append(sample_size)
|
| 213 |
+
footer_samples = fs.cat_ranges(paths, footer_starts, footer_ends)
|
| 214 |
+
|
| 215 |
+
# Check our footer samples and re-sample if necessary.
|
| 216 |
+
missing_footer_starts = footer_starts.copy()
|
| 217 |
+
large_footer = 0
|
| 218 |
+
for i, path in enumerate(paths):
|
| 219 |
+
footer_size = int.from_bytes(footer_samples[i][-8:-4], "little")
|
| 220 |
+
real_footer_start = file_sizes[i] - (footer_size + 8)
|
| 221 |
+
if real_footer_start < footer_starts[i]:
|
| 222 |
+
missing_footer_starts[i] = real_footer_start
|
| 223 |
+
large_footer = max(large_footer, (footer_size + 8))
|
| 224 |
+
if large_footer:
|
| 225 |
+
warnings.warn(
|
| 226 |
+
f"Not enough data was used to sample the parquet footer. "
|
| 227 |
+
f"Try setting footer_sample_size >= {large_footer}."
|
| 228 |
+
)
|
| 229 |
+
for i, block in enumerate(
|
| 230 |
+
fs.cat_ranges(
|
| 231 |
+
paths,
|
| 232 |
+
missing_footer_starts,
|
| 233 |
+
footer_starts,
|
| 234 |
+
)
|
| 235 |
+
):
|
| 236 |
+
footer_samples[i] = block + footer_samples[i]
|
| 237 |
+
footer_starts[i] = missing_footer_starts[i]
|
| 238 |
+
|
| 239 |
+
# Calculate required byte ranges for each path
|
| 240 |
+
for i, path in enumerate(paths):
|
| 241 |
+
|
| 242 |
+
# Deal with small-file case.
|
| 243 |
+
# Just include all remaining bytes of the file
|
| 244 |
+
# in a single range.
|
| 245 |
+
if file_sizes[i] < max_block:
|
| 246 |
+
if footer_starts[i] > 0:
|
| 247 |
+
# Only need to transfer the data if the
|
| 248 |
+
# footer sample isn't already the whole file
|
| 249 |
+
data_paths.append(path)
|
| 250 |
+
data_starts.append(0)
|
| 251 |
+
data_ends.append(footer_starts[i])
|
| 252 |
+
continue
|
| 253 |
+
|
| 254 |
+
# Use "engine" to collect data byte ranges
|
| 255 |
+
path_data_starts, path_data_ends = engine._parquet_byte_ranges(
|
| 256 |
+
columns,
|
| 257 |
+
row_groups=row_groups,
|
| 258 |
+
footer=footer_samples[i],
|
| 259 |
+
footer_start=footer_starts[i],
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
data_paths += [path] * len(path_data_starts)
|
| 263 |
+
data_starts += path_data_starts
|
| 264 |
+
data_ends += path_data_ends
|
| 265 |
+
|
| 266 |
+
# Merge adjacent offset ranges
|
| 267 |
+
data_paths, data_starts, data_ends = merge_offset_ranges(
|
| 268 |
+
data_paths,
|
| 269 |
+
data_starts,
|
| 270 |
+
data_ends,
|
| 271 |
+
max_gap=max_gap,
|
| 272 |
+
max_block=max_block,
|
| 273 |
+
sort=False, # Should already be sorted
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Start by populating `result` with footer samples
|
| 277 |
+
for i, path in enumerate(paths):
|
| 278 |
+
result[path] = {(footer_starts[i], footer_ends[i]): footer_samples[i]}
|
| 279 |
+
|
| 280 |
+
# Transfer the data byte-ranges into local memory
|
| 281 |
+
_transfer_ranges(fs, result, data_paths, data_starts, data_ends)
|
| 282 |
+
|
| 283 |
+
# Add b"PAR1" to header if necessary
|
| 284 |
+
if add_header_magic:
|
| 285 |
+
_add_header_magic(result)
|
| 286 |
+
|
| 287 |
+
return result
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def _get_parquet_byte_ranges_from_metadata(
|
| 291 |
+
metadata,
|
| 292 |
+
fs,
|
| 293 |
+
engine,
|
| 294 |
+
columns=None,
|
| 295 |
+
row_groups=None,
|
| 296 |
+
max_gap=64_000,
|
| 297 |
+
max_block=256_000_000,
|
| 298 |
+
):
|
| 299 |
+
"""Simplified version of `_get_parquet_byte_ranges` for
|
| 300 |
+
the case that an engine-specific `metadata` object is
|
| 301 |
+
provided, and the remote footer metadata does not need to
|
| 302 |
+
be transferred before calculating the required byte ranges.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
# Use "engine" to collect data byte ranges
|
| 306 |
+
data_paths, data_starts, data_ends = engine._parquet_byte_ranges(
|
| 307 |
+
columns,
|
| 308 |
+
row_groups=row_groups,
|
| 309 |
+
metadata=metadata,
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
# Merge adjacent offset ranges
|
| 313 |
+
data_paths, data_starts, data_ends = merge_offset_ranges(
|
| 314 |
+
data_paths,
|
| 315 |
+
data_starts,
|
| 316 |
+
data_ends,
|
| 317 |
+
max_gap=max_gap,
|
| 318 |
+
max_block=max_block,
|
| 319 |
+
sort=False, # Should be sorted
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
# Transfer the data byte-ranges into local memory
|
| 323 |
+
result = {fn: {} for fn in list(set(data_paths))}
|
| 324 |
+
_transfer_ranges(fs, result, data_paths, data_starts, data_ends)
|
| 325 |
+
|
| 326 |
+
# Add b"PAR1" to header
|
| 327 |
+
_add_header_magic(result)
|
| 328 |
+
|
| 329 |
+
return result
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def _transfer_ranges(fs, blocks, paths, starts, ends):
|
| 333 |
+
# Use cat_ranges to gather the data byte_ranges
|
| 334 |
+
ranges = (paths, starts, ends)
|
| 335 |
+
for path, start, stop, data in zip(*ranges, fs.cat_ranges(*ranges)):
|
| 336 |
+
blocks[path][(start, stop)] = data
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def _add_header_magic(data):
|
| 340 |
+
# Add b"PAR1" to file headers
|
| 341 |
+
for i, path in enumerate(list(data.keys())):
|
| 342 |
+
add_magic = True
|
| 343 |
+
for k in data[path].keys():
|
| 344 |
+
if k[0] == 0 and k[1] >= 4:
|
| 345 |
+
add_magic = False
|
| 346 |
+
break
|
| 347 |
+
if add_magic:
|
| 348 |
+
data[path][(0, 4)] = b"PAR1"
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
def _set_engine(engine_str):
|
| 352 |
+
|
| 353 |
+
# Define a list of parquet engines to try
|
| 354 |
+
if engine_str == "auto":
|
| 355 |
+
try_engines = ("fastparquet", "pyarrow")
|
| 356 |
+
elif not isinstance(engine_str, str):
|
| 357 |
+
raise ValueError(
|
| 358 |
+
"Failed to set parquet engine! "
|
| 359 |
+
"Please pass 'fastparquet', 'pyarrow', or 'auto'"
|
| 360 |
+
)
|
| 361 |
+
elif engine_str not in ("fastparquet", "pyarrow"):
|
| 362 |
+
raise ValueError(f"{engine_str} engine not supported by `fsspec.parquet`")
|
| 363 |
+
else:
|
| 364 |
+
try_engines = [engine_str]
|
| 365 |
+
|
| 366 |
+
# Try importing the engines in `try_engines`,
|
| 367 |
+
# and choose the first one that succeeds
|
| 368 |
+
for engine in try_engines:
|
| 369 |
+
try:
|
| 370 |
+
if engine == "fastparquet":
|
| 371 |
+
return FastparquetEngine()
|
| 372 |
+
elif engine == "pyarrow":
|
| 373 |
+
return PyarrowEngine()
|
| 374 |
+
except ImportError:
|
| 375 |
+
pass
|
| 376 |
+
|
| 377 |
+
# Raise an error if a supported parquet engine
|
| 378 |
+
# was not found
|
| 379 |
+
raise ImportError(
|
| 380 |
+
f"The following parquet engines are not installed "
|
| 381 |
+
f"in your python environment: {try_engines}."
|
| 382 |
+
f"Please install 'fastparquert' or 'pyarrow' to "
|
| 383 |
+
f"utilize the `fsspec.parquet` module."
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
class FastparquetEngine:
|
| 388 |
+
|
| 389 |
+
# The purpose of the FastparquetEngine class is
|
| 390 |
+
# to check if fastparquet can be imported (on initialization)
|
| 391 |
+
# and to define a `_parquet_byte_ranges` method. In the
|
| 392 |
+
# future, this class may also be used to define other
|
| 393 |
+
# methods/logic that are specific to fastparquet.
|
| 394 |
+
|
| 395 |
+
def __init__(self):
|
| 396 |
+
import fastparquet as fp
|
| 397 |
+
|
| 398 |
+
self.fp = fp
|
| 399 |
+
|
| 400 |
+
def _row_group_filename(self, row_group, pf):
|
| 401 |
+
return pf.row_group_filename(row_group)
|
| 402 |
+
|
| 403 |
+
def _parquet_byte_ranges(
|
| 404 |
+
self,
|
| 405 |
+
columns,
|
| 406 |
+
row_groups=None,
|
| 407 |
+
metadata=None,
|
| 408 |
+
footer=None,
|
| 409 |
+
footer_start=None,
|
| 410 |
+
):
|
| 411 |
+
|
| 412 |
+
# Initialize offset ranges and define ParqetFile metadata
|
| 413 |
+
pf = metadata
|
| 414 |
+
data_paths, data_starts, data_ends = [], [], []
|
| 415 |
+
if pf is None:
|
| 416 |
+
pf = self.fp.ParquetFile(io.BytesIO(footer))
|
| 417 |
+
|
| 418 |
+
# Convert columns to a set and add any index columns
|
| 419 |
+
# specified in the pandas metadata (just in case)
|
| 420 |
+
column_set = None if columns is None else set(columns)
|
| 421 |
+
if column_set is not None and hasattr(pf, "pandas_metadata"):
|
| 422 |
+
md_index = [
|
| 423 |
+
ind
|
| 424 |
+
for ind in pf.pandas_metadata.get("index_columns", [])
|
| 425 |
+
# Ignore RangeIndex information
|
| 426 |
+
if not isinstance(ind, dict)
|
| 427 |
+
]
|
| 428 |
+
column_set |= set(md_index)
|
| 429 |
+
|
| 430 |
+
# Check if row_groups is a list of integers
|
| 431 |
+
# or a list of row-group metadata
|
| 432 |
+
if row_groups and not isinstance(row_groups[0], int):
|
| 433 |
+
# Input row_groups contains row-group metadata
|
| 434 |
+
row_group_indices = None
|
| 435 |
+
else:
|
| 436 |
+
# Input row_groups contains row-group indices
|
| 437 |
+
row_group_indices = row_groups
|
| 438 |
+
row_groups = pf.row_groups
|
| 439 |
+
|
| 440 |
+
# Loop through column chunks to add required byte ranges
|
| 441 |
+
for r, row_group in enumerate(row_groups):
|
| 442 |
+
# Skip this row-group if we are targeting
|
| 443 |
+
# specific row-groups
|
| 444 |
+
if row_group_indices is None or r in row_group_indices:
|
| 445 |
+
|
| 446 |
+
# Find the target parquet-file path for `row_group`
|
| 447 |
+
fn = self._row_group_filename(row_group, pf)
|
| 448 |
+
|
| 449 |
+
for column in row_group.columns:
|
| 450 |
+
name = column.meta_data.path_in_schema[0]
|
| 451 |
+
# Skip this column if we are targeting a
|
| 452 |
+
# specific columns
|
| 453 |
+
if column_set is None or name in column_set:
|
| 454 |
+
file_offset0 = column.meta_data.dictionary_page_offset
|
| 455 |
+
if file_offset0 is None:
|
| 456 |
+
file_offset0 = column.meta_data.data_page_offset
|
| 457 |
+
num_bytes = column.meta_data.total_compressed_size
|
| 458 |
+
if footer_start is None or file_offset0 < footer_start:
|
| 459 |
+
data_paths.append(fn)
|
| 460 |
+
data_starts.append(file_offset0)
|
| 461 |
+
data_ends.append(
|
| 462 |
+
min(
|
| 463 |
+
file_offset0 + num_bytes,
|
| 464 |
+
footer_start or (file_offset0 + num_bytes),
|
| 465 |
+
)
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
if metadata:
|
| 469 |
+
# The metadata in this call may map to multiple
|
| 470 |
+
# file paths. Need to include `data_paths`
|
| 471 |
+
return data_paths, data_starts, data_ends
|
| 472 |
+
return data_starts, data_ends
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
class PyarrowEngine:
|
| 476 |
+
|
| 477 |
+
# The purpose of the PyarrowEngine class is
|
| 478 |
+
# to check if pyarrow can be imported (on initialization)
|
| 479 |
+
# and to define a `_parquet_byte_ranges` method. In the
|
| 480 |
+
# future, this class may also be used to define other
|
| 481 |
+
# methods/logic that are specific to pyarrow.
|
| 482 |
+
|
| 483 |
+
def __init__(self):
|
| 484 |
+
import pyarrow.parquet as pq
|
| 485 |
+
|
| 486 |
+
self.pq = pq
|
| 487 |
+
|
| 488 |
+
def _row_group_filename(self, row_group, metadata):
|
| 489 |
+
raise NotImplementedError
|
| 490 |
+
|
| 491 |
+
def _parquet_byte_ranges(
|
| 492 |
+
self,
|
| 493 |
+
columns,
|
| 494 |
+
row_groups=None,
|
| 495 |
+
metadata=None,
|
| 496 |
+
footer=None,
|
| 497 |
+
footer_start=None,
|
| 498 |
+
):
|
| 499 |
+
|
| 500 |
+
if metadata is not None:
|
| 501 |
+
raise ValueError("metadata input not supported for PyarrowEngine")
|
| 502 |
+
|
| 503 |
+
data_starts, data_ends = [], []
|
| 504 |
+
md = self.pq.ParquetFile(io.BytesIO(footer)).metadata
|
| 505 |
+
|
| 506 |
+
# Convert columns to a set and add any index columns
|
| 507 |
+
# specified in the pandas metadata (just in case)
|
| 508 |
+
column_set = None if columns is None else set(columns)
|
| 509 |
+
if column_set is not None:
|
| 510 |
+
schema = md.schema.to_arrow_schema()
|
| 511 |
+
has_pandas_metadata = (
|
| 512 |
+
schema.metadata is not None and b"pandas" in schema.metadata
|
| 513 |
+
)
|
| 514 |
+
if has_pandas_metadata:
|
| 515 |
+
md_index = [
|
| 516 |
+
ind
|
| 517 |
+
for ind in json.loads(
|
| 518 |
+
schema.metadata[b"pandas"].decode("utf8")
|
| 519 |
+
).get("index_columns", [])
|
| 520 |
+
# Ignore RangeIndex information
|
| 521 |
+
if not isinstance(ind, dict)
|
| 522 |
+
]
|
| 523 |
+
column_set |= set(md_index)
|
| 524 |
+
|
| 525 |
+
# Loop through column chunks to add required byte ranges
|
| 526 |
+
for r in range(md.num_row_groups):
|
| 527 |
+
# Skip this row-group if we are targeting
|
| 528 |
+
# specific row-groups
|
| 529 |
+
if row_groups is None or r in row_groups:
|
| 530 |
+
row_group = md.row_group(r)
|
| 531 |
+
for c in range(row_group.num_columns):
|
| 532 |
+
column = row_group.column(c)
|
| 533 |
+
name = column.path_in_schema
|
| 534 |
+
# Skip this column if we are targeting a
|
| 535 |
+
# specific columns
|
| 536 |
+
split_name = name.split(".")[0]
|
| 537 |
+
if (
|
| 538 |
+
column_set is None
|
| 539 |
+
or name in column_set
|
| 540 |
+
or split_name in column_set
|
| 541 |
+
):
|
| 542 |
+
file_offset0 = column.dictionary_page_offset
|
| 543 |
+
if file_offset0 is None:
|
| 544 |
+
file_offset0 = column.data_page_offset
|
| 545 |
+
num_bytes = column.total_compressed_size
|
| 546 |
+
if file_offset0 < footer_start:
|
| 547 |
+
data_starts.append(file_offset0)
|
| 548 |
+
data_ends.append(
|
| 549 |
+
min(file_offset0 + num_bytes, footer_start)
|
| 550 |
+
)
|
| 551 |
+
return data_starts, data_ends
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/registry.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
|
| 3 |
+
__all__ = ["registry", "get_filesystem_class", "default"]
|
| 4 |
+
|
| 5 |
+
# mapping protocol: implementation class object
|
| 6 |
+
_registry = {} # internal, mutable
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ReadOnlyError(TypeError):
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ReadOnlyRegistry(dict):
|
| 14 |
+
"""Dict-like registry, but immutable
|
| 15 |
+
|
| 16 |
+
Maps backend name to implementation class
|
| 17 |
+
|
| 18 |
+
To add backend implementations, use ``register_implementation``
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def __init__(self, target):
|
| 22 |
+
self.target = target
|
| 23 |
+
|
| 24 |
+
def __getitem__(self, item):
|
| 25 |
+
return self.target[item]
|
| 26 |
+
|
| 27 |
+
def __delitem__(self, key):
|
| 28 |
+
raise ReadOnlyError
|
| 29 |
+
|
| 30 |
+
def __setitem__(self, key, value):
|
| 31 |
+
raise ReadOnlyError
|
| 32 |
+
|
| 33 |
+
def clear(self):
|
| 34 |
+
raise ReadOnlyError
|
| 35 |
+
|
| 36 |
+
def __contains__(self, item):
|
| 37 |
+
return item in self.target
|
| 38 |
+
|
| 39 |
+
def __iter__(self):
|
| 40 |
+
yield from self.target
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def register_implementation(name, cls, clobber=True, errtxt=None):
|
| 44 |
+
"""Add implementation class to the registry
|
| 45 |
+
|
| 46 |
+
Parameters
|
| 47 |
+
----------
|
| 48 |
+
name: str
|
| 49 |
+
Protocol name to associate with the class
|
| 50 |
+
cls: class or str
|
| 51 |
+
if a class: fsspec-compliant implementation class (normally inherits from
|
| 52 |
+
``fsspec.AbstractFileSystem``, gets added straight to the registry. If a
|
| 53 |
+
str, the full path to an implementation class like package.module.class,
|
| 54 |
+
which gets added to known_implementations,
|
| 55 |
+
so the import is deferred until the filesystem is actually used.
|
| 56 |
+
clobber: bool (optional)
|
| 57 |
+
Whether to overwrite a protocol with the same name; if False, will raise
|
| 58 |
+
instead.
|
| 59 |
+
errtxt: str (optional)
|
| 60 |
+
If given, then a failure to import the given class will result in this
|
| 61 |
+
text being given.
|
| 62 |
+
"""
|
| 63 |
+
if isinstance(cls, str):
|
| 64 |
+
if name in known_implementations and clobber is False:
|
| 65 |
+
raise ValueError(
|
| 66 |
+
"Name (%s) already in the known_implementations and clobber "
|
| 67 |
+
"is False" % name
|
| 68 |
+
)
|
| 69 |
+
known_implementations[name] = {
|
| 70 |
+
"class": cls,
|
| 71 |
+
"err": errtxt or "%s import failed for protocol %s" % (cls, name),
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
else:
|
| 75 |
+
if name in registry and clobber is False:
|
| 76 |
+
raise ValueError(
|
| 77 |
+
"Name (%s) already in the registry and clobber is False" % name
|
| 78 |
+
)
|
| 79 |
+
_registry[name] = cls
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
registry = ReadOnlyRegistry(_registry)
|
| 83 |
+
default = "file"
|
| 84 |
+
|
| 85 |
+
# protocols mapped to the class which implements them. This dict can
|
| 86 |
+
# updated with register_implementation
|
| 87 |
+
known_implementations = {
|
| 88 |
+
"file": {"class": "fsspec.implementations.local.LocalFileSystem"},
|
| 89 |
+
"memory": {"class": "fsspec.implementations.memory.MemoryFileSystem"},
|
| 90 |
+
"dropbox": {
|
| 91 |
+
"class": "dropboxdrivefs.DropboxDriveFileSystem",
|
| 92 |
+
"err": (
|
| 93 |
+
'DropboxFileSystem requires "dropboxdrivefs",'
|
| 94 |
+
'"requests" and "dropbox" to be installed'
|
| 95 |
+
),
|
| 96 |
+
},
|
| 97 |
+
"http": {
|
| 98 |
+
"class": "fsspec.implementations.http.HTTPFileSystem",
|
| 99 |
+
"err": 'HTTPFileSystem requires "requests" and "aiohttp" to be installed',
|
| 100 |
+
},
|
| 101 |
+
"https": {
|
| 102 |
+
"class": "fsspec.implementations.http.HTTPFileSystem",
|
| 103 |
+
"err": 'HTTPFileSystem requires "requests" and "aiohttp" to be installed',
|
| 104 |
+
},
|
| 105 |
+
"zip": {"class": "fsspec.implementations.zip.ZipFileSystem"},
|
| 106 |
+
"tar": {"class": "fsspec.implementations.tar.TarFileSystem"},
|
| 107 |
+
"gcs": {
|
| 108 |
+
"class": "gcsfs.GCSFileSystem",
|
| 109 |
+
"err": "Please install gcsfs to access Google Storage",
|
| 110 |
+
},
|
| 111 |
+
"gs": {
|
| 112 |
+
"class": "gcsfs.GCSFileSystem",
|
| 113 |
+
"err": "Please install gcsfs to access Google Storage",
|
| 114 |
+
},
|
| 115 |
+
"gdrive": {
|
| 116 |
+
"class": "gdrivefs.GoogleDriveFileSystem",
|
| 117 |
+
"err": "Please install gdrivefs for access to Google Drive",
|
| 118 |
+
},
|
| 119 |
+
"sftp": {
|
| 120 |
+
"class": "fsspec.implementations.sftp.SFTPFileSystem",
|
| 121 |
+
"err": 'SFTPFileSystem requires "paramiko" to be installed',
|
| 122 |
+
},
|
| 123 |
+
"ssh": {
|
| 124 |
+
"class": "fsspec.implementations.sftp.SFTPFileSystem",
|
| 125 |
+
"err": 'SFTPFileSystem requires "paramiko" to be installed',
|
| 126 |
+
},
|
| 127 |
+
"ftp": {"class": "fsspec.implementations.ftp.FTPFileSystem"},
|
| 128 |
+
"hdfs": {
|
| 129 |
+
"class": "fsspec.implementations.hdfs.PyArrowHDFS",
|
| 130 |
+
"err": "pyarrow and local java libraries required for HDFS",
|
| 131 |
+
},
|
| 132 |
+
"arrow_hdfs": {
|
| 133 |
+
"class": "fsspec.implementations.arrow.HadoopFileSystem",
|
| 134 |
+
"err": "pyarrow and local java libraries required for HDFS",
|
| 135 |
+
},
|
| 136 |
+
"webhdfs": {
|
| 137 |
+
"class": "fsspec.implementations.webhdfs.WebHDFS",
|
| 138 |
+
"err": 'webHDFS access requires "requests" to be installed',
|
| 139 |
+
},
|
| 140 |
+
"s3": {"class": "s3fs.S3FileSystem", "err": "Install s3fs to access S3"},
|
| 141 |
+
"s3a": {"class": "s3fs.S3FileSystem", "err": "Install s3fs to access S3"},
|
| 142 |
+
"wandb": {"class": "wandbfs.WandbFS", "err": "Install wandbfs to access wandb"},
|
| 143 |
+
"oci": {
|
| 144 |
+
"class": "ocifs.OCIFileSystem",
|
| 145 |
+
"err": "Install ocifs to access OCI Object Storage",
|
| 146 |
+
},
|
| 147 |
+
"adl": {
|
| 148 |
+
"class": "adlfs.AzureDatalakeFileSystem",
|
| 149 |
+
"err": "Install adlfs to access Azure Datalake Gen1",
|
| 150 |
+
},
|
| 151 |
+
"abfs": {
|
| 152 |
+
"class": "adlfs.AzureBlobFileSystem",
|
| 153 |
+
"err": "Install adlfs to access Azure Datalake Gen2 and Azure Blob Storage",
|
| 154 |
+
},
|
| 155 |
+
"az": {
|
| 156 |
+
"class": "adlfs.AzureBlobFileSystem",
|
| 157 |
+
"err": "Install adlfs to access Azure Datalake Gen2 and Azure Blob Storage",
|
| 158 |
+
},
|
| 159 |
+
"cached": {"class": "fsspec.implementations.cached.CachingFileSystem"},
|
| 160 |
+
"blockcache": {"class": "fsspec.implementations.cached.CachingFileSystem"},
|
| 161 |
+
"filecache": {"class": "fsspec.implementations.cached.WholeFileCacheFileSystem"},
|
| 162 |
+
"simplecache": {"class": "fsspec.implementations.cached.SimpleCacheFileSystem"},
|
| 163 |
+
"dask": {
|
| 164 |
+
"class": "fsspec.implementations.dask.DaskWorkerFileSystem",
|
| 165 |
+
"err": "Install dask distributed to access worker file system",
|
| 166 |
+
},
|
| 167 |
+
"dbfs": {
|
| 168 |
+
"class": "fsspec.implementations.dbfs.DatabricksFileSystem",
|
| 169 |
+
"err": "Install the requests package to use the DatabricksFileSystem",
|
| 170 |
+
},
|
| 171 |
+
"github": {
|
| 172 |
+
"class": "fsspec.implementations.github.GithubFileSystem",
|
| 173 |
+
"err": "Install the requests package to use the github FS",
|
| 174 |
+
},
|
| 175 |
+
"git": {
|
| 176 |
+
"class": "fsspec.implementations.git.GitFileSystem",
|
| 177 |
+
"err": "Install pygit2 to browse local git repos",
|
| 178 |
+
},
|
| 179 |
+
"smb": {
|
| 180 |
+
"class": "fsspec.implementations.smb.SMBFileSystem",
|
| 181 |
+
"err": 'SMB requires "smbprotocol" or "smbprotocol[kerberos]" installed',
|
| 182 |
+
},
|
| 183 |
+
"jupyter": {
|
| 184 |
+
"class": "fsspec.implementations.jupyter.JupyterFileSystem",
|
| 185 |
+
"err": "Jupyter FS requires requests to be installed",
|
| 186 |
+
},
|
| 187 |
+
"jlab": {
|
| 188 |
+
"class": "fsspec.implementations.jupyter.JupyterFileSystem",
|
| 189 |
+
"err": "Jupyter FS requires requests to be installed",
|
| 190 |
+
},
|
| 191 |
+
"libarchive": {
|
| 192 |
+
"class": "fsspec.implementations.libarchive.LibArchiveFileSystem",
|
| 193 |
+
"err": "LibArchive requires to be installed",
|
| 194 |
+
},
|
| 195 |
+
"reference": {"class": "fsspec.implementations.reference.ReferenceFileSystem"},
|
| 196 |
+
"generic": {"class": "fsspec.generic.GenericFileSystem"},
|
| 197 |
+
"oss": {
|
| 198 |
+
"class": "ossfs.OSSFileSystem",
|
| 199 |
+
"err": "Install ossfs to access Alibaba Object Storage System",
|
| 200 |
+
},
|
| 201 |
+
"webdav": {
|
| 202 |
+
"class": "webdav4.fsspec.WebdavFileSystem",
|
| 203 |
+
"err": "Install webdav4 to access WebDAV",
|
| 204 |
+
},
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def get_filesystem_class(protocol):
|
| 209 |
+
"""Fetch named protocol implementation from the registry
|
| 210 |
+
|
| 211 |
+
The dict ``known_implementations`` maps protocol names to the locations
|
| 212 |
+
of classes implementing the corresponding file-system. When used for the
|
| 213 |
+
first time, appropriate imports will happen and the class will be placed in
|
| 214 |
+
the registry. All subsequent calls will fetch directly from the registry.
|
| 215 |
+
|
| 216 |
+
Some protocol implementations require additional dependencies, and so the
|
| 217 |
+
import may fail. In this case, the string in the "err" field of the
|
| 218 |
+
``known_implementations`` will be given as the error message.
|
| 219 |
+
"""
|
| 220 |
+
if not protocol:
|
| 221 |
+
protocol = default
|
| 222 |
+
|
| 223 |
+
if protocol not in registry:
|
| 224 |
+
if protocol not in known_implementations:
|
| 225 |
+
raise ValueError("Protocol not known: %s" % protocol)
|
| 226 |
+
bit = known_implementations[protocol]
|
| 227 |
+
try:
|
| 228 |
+
register_implementation(protocol, _import_class(bit["class"]))
|
| 229 |
+
except ImportError as e:
|
| 230 |
+
raise ImportError(bit["err"]) from e
|
| 231 |
+
cls = registry[protocol]
|
| 232 |
+
if getattr(cls, "protocol", None) in ("abstract", None):
|
| 233 |
+
cls.protocol = protocol
|
| 234 |
+
|
| 235 |
+
return cls
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _import_class(cls, minv=None):
|
| 239 |
+
"""Take a string FQP and return the imported class or identifier
|
| 240 |
+
|
| 241 |
+
clas is of the form "package.module.klass" or "package.module:subobject.klass"
|
| 242 |
+
"""
|
| 243 |
+
if ":" in cls:
|
| 244 |
+
mod, name = cls.rsplit(":", 1)
|
| 245 |
+
mod = importlib.import_module(mod)
|
| 246 |
+
for part in name.split("."):
|
| 247 |
+
mod = getattr(mod, part)
|
| 248 |
+
return mod
|
| 249 |
+
else:
|
| 250 |
+
mod, name = cls.rsplit(".", 1)
|
| 251 |
+
mod = importlib.import_module(mod)
|
| 252 |
+
return getattr(mod, name)
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def filesystem(protocol, **storage_options):
|
| 256 |
+
"""Instantiate filesystems for given protocol and arguments
|
| 257 |
+
|
| 258 |
+
``storage_options`` are specific to the protocol being chosen, and are
|
| 259 |
+
passed directly to the class.
|
| 260 |
+
"""
|
| 261 |
+
cls = get_filesystem_class(protocol)
|
| 262 |
+
return cls(**storage_options)
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
def available_protocols():
|
| 266 |
+
"""Return a list of the implemented protocols.
|
| 267 |
+
|
| 268 |
+
Note that any given protocol may require extra packages to be importable.
|
| 269 |
+
"""
|
| 270 |
+
return list(known_implementations)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/spec.py
ADDED
|
@@ -0,0 +1,1697 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import threading
|
| 5 |
+
import warnings
|
| 6 |
+
import weakref
|
| 7 |
+
from errno import ESPIPE
|
| 8 |
+
from glob import has_magic
|
| 9 |
+
from hashlib import sha256
|
| 10 |
+
|
| 11 |
+
from .callbacks import _DEFAULT_CALLBACK
|
| 12 |
+
from .config import apply_config, conf
|
| 13 |
+
from .dircache import DirCache
|
| 14 |
+
from .transaction import Transaction
|
| 15 |
+
from .utils import (
|
| 16 |
+
_unstrip_protocol,
|
| 17 |
+
isfilelike,
|
| 18 |
+
other_paths,
|
| 19 |
+
read_block,
|
| 20 |
+
stringify_path,
|
| 21 |
+
tokenize,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger("fsspec")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def make_instance(cls, args, kwargs):
|
| 28 |
+
return cls(*args, **kwargs)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class _Cached(type):
|
| 32 |
+
"""
|
| 33 |
+
Metaclass for caching file system instances.
|
| 34 |
+
|
| 35 |
+
Notes
|
| 36 |
+
-----
|
| 37 |
+
Instances are cached according to
|
| 38 |
+
|
| 39 |
+
* The values of the class attributes listed in `_extra_tokenize_attributes`
|
| 40 |
+
* The arguments passed to ``__init__``.
|
| 41 |
+
|
| 42 |
+
This creates an additional reference to the filesystem, which prevents the
|
| 43 |
+
filesystem from being garbage collected when all *user* references go away.
|
| 44 |
+
A call to the :meth:`AbstractFileSystem.clear_instance_cache` must *also*
|
| 45 |
+
be made for a filesystem instance to be garbage collected.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(cls, *args, **kwargs):
|
| 49 |
+
super().__init__(*args, **kwargs)
|
| 50 |
+
# Note: we intentionally create a reference here, to avoid garbage
|
| 51 |
+
# collecting instances when all other references are gone. To really
|
| 52 |
+
# delete a FileSystem, the cache must be cleared.
|
| 53 |
+
if conf.get("weakref_instance_cache"): # pragma: no cover
|
| 54 |
+
# debug option for analysing fork/spawn conditions
|
| 55 |
+
cls._cache = weakref.WeakValueDictionary()
|
| 56 |
+
else:
|
| 57 |
+
cls._cache = {}
|
| 58 |
+
cls._pid = os.getpid()
|
| 59 |
+
|
| 60 |
+
def __call__(cls, *args, **kwargs):
|
| 61 |
+
kwargs = apply_config(cls, kwargs)
|
| 62 |
+
extra_tokens = tuple(
|
| 63 |
+
getattr(cls, attr, None) for attr in cls._extra_tokenize_attributes
|
| 64 |
+
)
|
| 65 |
+
token = tokenize(
|
| 66 |
+
cls, cls._pid, threading.get_ident(), *args, *extra_tokens, **kwargs
|
| 67 |
+
)
|
| 68 |
+
skip = kwargs.pop("skip_instance_cache", False)
|
| 69 |
+
if os.getpid() != cls._pid:
|
| 70 |
+
cls._cache.clear()
|
| 71 |
+
cls._pid = os.getpid()
|
| 72 |
+
if not skip and cls.cachable and token in cls._cache:
|
| 73 |
+
cls._latest = token
|
| 74 |
+
return cls._cache[token]
|
| 75 |
+
else:
|
| 76 |
+
obj = super().__call__(*args, **kwargs)
|
| 77 |
+
# Setting _fs_token here causes some static linters to complain.
|
| 78 |
+
obj._fs_token_ = token
|
| 79 |
+
obj.storage_args = args
|
| 80 |
+
obj.storage_options = kwargs
|
| 81 |
+
if obj.async_impl:
|
| 82 |
+
from .asyn import mirror_sync_methods
|
| 83 |
+
|
| 84 |
+
mirror_sync_methods(obj)
|
| 85 |
+
|
| 86 |
+
if cls.cachable and not skip:
|
| 87 |
+
cls._latest = token
|
| 88 |
+
cls._cache[token] = obj
|
| 89 |
+
return obj
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class AbstractFileSystem(metaclass=_Cached):
|
| 93 |
+
"""
|
| 94 |
+
An abstract super-class for pythonic file-systems
|
| 95 |
+
|
| 96 |
+
Implementations are expected to be compatible with or, better, subclass
|
| 97 |
+
from here.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
cachable = True # this class can be cached, instances reused
|
| 101 |
+
_cached = False
|
| 102 |
+
blocksize = 2**22
|
| 103 |
+
sep = "/"
|
| 104 |
+
protocol = "abstract"
|
| 105 |
+
_latest = None
|
| 106 |
+
async_impl = False
|
| 107 |
+
root_marker = "" # For some FSs, may require leading '/' or other character
|
| 108 |
+
|
| 109 |
+
#: Extra *class attributes* that should be considered when hashing.
|
| 110 |
+
_extra_tokenize_attributes = ()
|
| 111 |
+
|
| 112 |
+
def __init__(self, *args, **storage_options):
|
| 113 |
+
"""Create and configure file-system instance
|
| 114 |
+
|
| 115 |
+
Instances may be cachable, so if similar enough arguments are seen
|
| 116 |
+
a new instance is not required. The token attribute exists to allow
|
| 117 |
+
implementations to cache instances if they wish.
|
| 118 |
+
|
| 119 |
+
A reasonable default should be provided if there are no arguments.
|
| 120 |
+
|
| 121 |
+
Subclasses should call this method.
|
| 122 |
+
|
| 123 |
+
Parameters
|
| 124 |
+
----------
|
| 125 |
+
use_listings_cache, listings_expiry_time, max_paths:
|
| 126 |
+
passed to ``DirCache``, if the implementation supports
|
| 127 |
+
directory listing caching. Pass use_listings_cache=False
|
| 128 |
+
to disable such caching.
|
| 129 |
+
skip_instance_cache: bool
|
| 130 |
+
If this is a cachable implementation, pass True here to force
|
| 131 |
+
creating a new instance even if a matching instance exists, and prevent
|
| 132 |
+
storing this instance.
|
| 133 |
+
asynchronous: bool
|
| 134 |
+
loop: asyncio-compatible IOLoop or None
|
| 135 |
+
"""
|
| 136 |
+
if self._cached:
|
| 137 |
+
# reusing instance, don't change
|
| 138 |
+
return
|
| 139 |
+
self._cached = True
|
| 140 |
+
self._intrans = False
|
| 141 |
+
self._transaction = None
|
| 142 |
+
self._invalidated_caches_in_transaction = []
|
| 143 |
+
self.dircache = DirCache(**storage_options)
|
| 144 |
+
|
| 145 |
+
if storage_options.pop("add_docs", None):
|
| 146 |
+
warnings.warn("add_docs is no longer supported.", FutureWarning)
|
| 147 |
+
|
| 148 |
+
if storage_options.pop("add_aliases", None):
|
| 149 |
+
warnings.warn("add_aliases has been removed.", FutureWarning)
|
| 150 |
+
# This is set in _Cached
|
| 151 |
+
self._fs_token_ = None
|
| 152 |
+
|
| 153 |
+
@property
|
| 154 |
+
def _fs_token(self):
|
| 155 |
+
return self._fs_token_
|
| 156 |
+
|
| 157 |
+
def __dask_tokenize__(self):
|
| 158 |
+
return self._fs_token
|
| 159 |
+
|
| 160 |
+
def __hash__(self):
|
| 161 |
+
return int(self._fs_token, 16)
|
| 162 |
+
|
| 163 |
+
def __eq__(self, other):
|
| 164 |
+
return isinstance(other, type(self)) and self._fs_token == other._fs_token
|
| 165 |
+
|
| 166 |
+
def __reduce__(self):
|
| 167 |
+
return make_instance, (type(self), self.storage_args, self.storage_options)
|
| 168 |
+
|
| 169 |
+
@classmethod
|
| 170 |
+
def _strip_protocol(cls, path):
|
| 171 |
+
"""Turn path from fully-qualified to file-system-specific
|
| 172 |
+
|
| 173 |
+
May require FS-specific handling, e.g., for relative paths or links.
|
| 174 |
+
"""
|
| 175 |
+
if isinstance(path, list):
|
| 176 |
+
return [cls._strip_protocol(p) for p in path]
|
| 177 |
+
path = stringify_path(path)
|
| 178 |
+
protos = (cls.protocol,) if isinstance(cls.protocol, str) else cls.protocol
|
| 179 |
+
for protocol in protos:
|
| 180 |
+
if path.startswith(protocol + "://"):
|
| 181 |
+
path = path[len(protocol) + 3 :]
|
| 182 |
+
elif path.startswith(protocol + "::"):
|
| 183 |
+
path = path[len(protocol) + 2 :]
|
| 184 |
+
path = path.rstrip("/")
|
| 185 |
+
# use of root_marker to make minimum required path, e.g., "/"
|
| 186 |
+
return path or cls.root_marker
|
| 187 |
+
|
| 188 |
+
def unstrip_protocol(self, name):
|
| 189 |
+
"""Format FS-specific path to generic, including protocol"""
|
| 190 |
+
if isinstance(self.protocol, str):
|
| 191 |
+
if name.startswith(self.protocol):
|
| 192 |
+
return name
|
| 193 |
+
return self.protocol + "://" + name
|
| 194 |
+
else:
|
| 195 |
+
if name.startswith(tuple(self.protocol)):
|
| 196 |
+
return name
|
| 197 |
+
return self.protocol[0] + "://" + name
|
| 198 |
+
|
| 199 |
+
@staticmethod
|
| 200 |
+
def _get_kwargs_from_urls(path):
|
| 201 |
+
"""If kwargs can be encoded in the paths, extract them here
|
| 202 |
+
|
| 203 |
+
This should happen before instantiation of the class; incoming paths
|
| 204 |
+
then should be amended to strip the options in methods.
|
| 205 |
+
|
| 206 |
+
Examples may look like an sftp path "sftp://user@host:/my/path", where
|
| 207 |
+
the user and host should become kwargs and later get stripped.
|
| 208 |
+
"""
|
| 209 |
+
# by default, nothing happens
|
| 210 |
+
return {}
|
| 211 |
+
|
| 212 |
+
@classmethod
|
| 213 |
+
def current(cls):
|
| 214 |
+
"""Return the most recently instantiated FileSystem
|
| 215 |
+
|
| 216 |
+
If no instance has been created, then create one with defaults
|
| 217 |
+
"""
|
| 218 |
+
if cls._latest in cls._cache:
|
| 219 |
+
return cls._cache[cls._latest]
|
| 220 |
+
return cls()
|
| 221 |
+
|
| 222 |
+
@property
|
| 223 |
+
def transaction(self):
|
| 224 |
+
"""A context within which files are committed together upon exit
|
| 225 |
+
|
| 226 |
+
Requires the file class to implement `.commit()` and `.discard()`
|
| 227 |
+
for the normal and exception cases.
|
| 228 |
+
"""
|
| 229 |
+
if self._transaction is None:
|
| 230 |
+
self._transaction = Transaction(self)
|
| 231 |
+
return self._transaction
|
| 232 |
+
|
| 233 |
+
def start_transaction(self):
|
| 234 |
+
"""Begin write transaction for deferring files, non-context version"""
|
| 235 |
+
self._intrans = True
|
| 236 |
+
self._transaction = Transaction(self)
|
| 237 |
+
return self.transaction
|
| 238 |
+
|
| 239 |
+
def end_transaction(self):
|
| 240 |
+
"""Finish write transaction, non-context version"""
|
| 241 |
+
self.transaction.complete()
|
| 242 |
+
self._transaction = None
|
| 243 |
+
# The invalid cache must be cleared after the transcation is completed.
|
| 244 |
+
for path in self._invalidated_caches_in_transaction:
|
| 245 |
+
self.invalidate_cache(path)
|
| 246 |
+
self._invalidated_caches_in_transaction.clear()
|
| 247 |
+
|
| 248 |
+
def invalidate_cache(self, path=None):
|
| 249 |
+
"""
|
| 250 |
+
Discard any cached directory information
|
| 251 |
+
|
| 252 |
+
Parameters
|
| 253 |
+
----------
|
| 254 |
+
path: string or None
|
| 255 |
+
If None, clear all listings cached else listings at or under given
|
| 256 |
+
path.
|
| 257 |
+
"""
|
| 258 |
+
# Not necessary to implement invalidation mechanism, may have no cache.
|
| 259 |
+
# But if have, you should call this method of parent class from your
|
| 260 |
+
# subclass to ensure expiring caches after transacations correctly.
|
| 261 |
+
# See the implementation of FTPFileSystem in ftp.py
|
| 262 |
+
if self._intrans:
|
| 263 |
+
self._invalidated_caches_in_transaction.append(path)
|
| 264 |
+
|
| 265 |
+
def mkdir(self, path, create_parents=True, **kwargs):
|
| 266 |
+
"""
|
| 267 |
+
Create directory entry at path
|
| 268 |
+
|
| 269 |
+
For systems that don't have true directories, may create an for
|
| 270 |
+
this instance only and not touch the real filesystem
|
| 271 |
+
|
| 272 |
+
Parameters
|
| 273 |
+
----------
|
| 274 |
+
path: str
|
| 275 |
+
location
|
| 276 |
+
create_parents: bool
|
| 277 |
+
if True, this is equivalent to ``makedirs``
|
| 278 |
+
kwargs:
|
| 279 |
+
may be permissions, etc.
|
| 280 |
+
"""
|
| 281 |
+
pass # not necessary to implement, may not have directories
|
| 282 |
+
|
| 283 |
+
def makedirs(self, path, exist_ok=False):
|
| 284 |
+
"""Recursively make directories
|
| 285 |
+
|
| 286 |
+
Creates directory at path and any intervening required directories.
|
| 287 |
+
Raises exception if, for instance, the path already exists but is a
|
| 288 |
+
file.
|
| 289 |
+
|
| 290 |
+
Parameters
|
| 291 |
+
----------
|
| 292 |
+
path: str
|
| 293 |
+
leaf directory name
|
| 294 |
+
exist_ok: bool (False)
|
| 295 |
+
If False, will error if the target already exists
|
| 296 |
+
"""
|
| 297 |
+
pass # not necessary to implement, may not have directories
|
| 298 |
+
|
| 299 |
+
def rmdir(self, path):
|
| 300 |
+
"""Remove a directory, if empty"""
|
| 301 |
+
pass # not necessary to implement, may not have directories
|
| 302 |
+
|
| 303 |
+
def ls(self, path, detail=True, **kwargs):
|
| 304 |
+
"""List objects at path.
|
| 305 |
+
|
| 306 |
+
This should include subdirectories and files at that location. The
|
| 307 |
+
difference between a file and a directory must be clear when details
|
| 308 |
+
are requested.
|
| 309 |
+
|
| 310 |
+
The specific keys, or perhaps a FileInfo class, or similar, is TBD,
|
| 311 |
+
but must be consistent across implementations.
|
| 312 |
+
Must include:
|
| 313 |
+
|
| 314 |
+
- full path to the entry (without protocol)
|
| 315 |
+
- size of the entry, in bytes. If the value cannot be determined, will
|
| 316 |
+
be ``None``.
|
| 317 |
+
- type of entry, "file", "directory" or other
|
| 318 |
+
|
| 319 |
+
Additional information
|
| 320 |
+
may be present, aproriate to the file-system, e.g., generation,
|
| 321 |
+
checksum, etc.
|
| 322 |
+
|
| 323 |
+
May use refresh=True|False to allow use of self._ls_from_cache to
|
| 324 |
+
check for a saved listing and avoid calling the backend. This would be
|
| 325 |
+
common where listing may be expensive.
|
| 326 |
+
|
| 327 |
+
Parameters
|
| 328 |
+
----------
|
| 329 |
+
path: str
|
| 330 |
+
detail: bool
|
| 331 |
+
if True, gives a list of dictionaries, where each is the same as
|
| 332 |
+
the result of ``info(path)``. If False, gives a list of paths
|
| 333 |
+
(str).
|
| 334 |
+
kwargs: may have additional backend-specific options, such as version
|
| 335 |
+
information
|
| 336 |
+
|
| 337 |
+
Returns
|
| 338 |
+
-------
|
| 339 |
+
List of strings if detail is False, or list of directory information
|
| 340 |
+
dicts if detail is True.
|
| 341 |
+
"""
|
| 342 |
+
raise NotImplementedError
|
| 343 |
+
|
| 344 |
+
def _ls_from_cache(self, path):
|
| 345 |
+
"""Check cache for listing
|
| 346 |
+
|
| 347 |
+
Returns listing, if found (may me empty list for a directly that exists
|
| 348 |
+
but contains nothing), None if not in cache.
|
| 349 |
+
"""
|
| 350 |
+
parent = self._parent(path)
|
| 351 |
+
if path.rstrip("/") in self.dircache:
|
| 352 |
+
return self.dircache[path.rstrip("/")]
|
| 353 |
+
try:
|
| 354 |
+
files = [
|
| 355 |
+
f
|
| 356 |
+
for f in self.dircache[parent]
|
| 357 |
+
if f["name"] == path
|
| 358 |
+
or (f["name"] == path.rstrip("/") and f["type"] == "directory")
|
| 359 |
+
]
|
| 360 |
+
if len(files) == 0:
|
| 361 |
+
# parent dir was listed but did not contain this file
|
| 362 |
+
raise FileNotFoundError(path)
|
| 363 |
+
return files
|
| 364 |
+
except KeyError:
|
| 365 |
+
pass
|
| 366 |
+
|
| 367 |
+
def walk(self, path, maxdepth=None, **kwargs):
|
| 368 |
+
"""Return all files belows path
|
| 369 |
+
|
| 370 |
+
List all files, recursing into subdirectories; output is iterator-style,
|
| 371 |
+
like ``os.walk()``. For a simple list of files, ``find()`` is available.
|
| 372 |
+
|
| 373 |
+
Note that the "files" outputted will include anything that is not
|
| 374 |
+
a directory, such as links.
|
| 375 |
+
|
| 376 |
+
Parameters
|
| 377 |
+
----------
|
| 378 |
+
path: str
|
| 379 |
+
Root to recurse into
|
| 380 |
+
maxdepth: int
|
| 381 |
+
Maximum recursion depth. None means limitless, but not recommended
|
| 382 |
+
on link-based file-systems.
|
| 383 |
+
kwargs: passed to ``ls``
|
| 384 |
+
"""
|
| 385 |
+
path = self._strip_protocol(path)
|
| 386 |
+
full_dirs = {}
|
| 387 |
+
dirs = {}
|
| 388 |
+
files = {}
|
| 389 |
+
|
| 390 |
+
detail = kwargs.pop("detail", False)
|
| 391 |
+
try:
|
| 392 |
+
listing = self.ls(path, detail=True, **kwargs)
|
| 393 |
+
except (FileNotFoundError, IOError):
|
| 394 |
+
if detail:
|
| 395 |
+
return path, {}, {}
|
| 396 |
+
return path, [], []
|
| 397 |
+
|
| 398 |
+
for info in listing:
|
| 399 |
+
# each info name must be at least [path]/part , but here
|
| 400 |
+
# we check also for names like [path]/part/
|
| 401 |
+
pathname = info["name"].rstrip("/")
|
| 402 |
+
name = pathname.rsplit("/", 1)[-1]
|
| 403 |
+
if info["type"] == "directory" and pathname != path:
|
| 404 |
+
# do not include "self" path
|
| 405 |
+
full_dirs[pathname] = info
|
| 406 |
+
dirs[name] = info
|
| 407 |
+
elif pathname == path:
|
| 408 |
+
# file-like with same name as give path
|
| 409 |
+
files[""] = info
|
| 410 |
+
else:
|
| 411 |
+
files[name] = info
|
| 412 |
+
|
| 413 |
+
if detail:
|
| 414 |
+
yield path, dirs, files
|
| 415 |
+
else:
|
| 416 |
+
yield path, list(dirs), list(files)
|
| 417 |
+
|
| 418 |
+
if maxdepth is not None:
|
| 419 |
+
maxdepth -= 1
|
| 420 |
+
if maxdepth < 1:
|
| 421 |
+
return
|
| 422 |
+
|
| 423 |
+
for d in full_dirs:
|
| 424 |
+
yield from self.walk(d, maxdepth=maxdepth, detail=detail, **kwargs)
|
| 425 |
+
|
| 426 |
+
def find(self, path, maxdepth=None, withdirs=False, detail=False, **kwargs):
|
| 427 |
+
"""List all files below path.
|
| 428 |
+
|
| 429 |
+
Like posix ``find`` command without conditions
|
| 430 |
+
|
| 431 |
+
Parameters
|
| 432 |
+
----------
|
| 433 |
+
path : str
|
| 434 |
+
maxdepth: int or None
|
| 435 |
+
If not None, the maximum number of levels to descend
|
| 436 |
+
withdirs: bool
|
| 437 |
+
Whether to include directory paths in the output. This is True
|
| 438 |
+
when used by glob, but users usually only want files.
|
| 439 |
+
kwargs are passed to ``ls``.
|
| 440 |
+
"""
|
| 441 |
+
# TODO: allow equivalent of -name parameter
|
| 442 |
+
path = self._strip_protocol(path)
|
| 443 |
+
out = dict()
|
| 444 |
+
for _, dirs, files in self.walk(path, maxdepth, detail=True, **kwargs):
|
| 445 |
+
if withdirs:
|
| 446 |
+
files.update(dirs)
|
| 447 |
+
out.update({info["name"]: info for name, info in files.items()})
|
| 448 |
+
if not out and self.isfile(path):
|
| 449 |
+
# walk works on directories, but find should also return [path]
|
| 450 |
+
# when path happens to be a file
|
| 451 |
+
out[path] = {}
|
| 452 |
+
names = sorted(out)
|
| 453 |
+
if not detail:
|
| 454 |
+
return names
|
| 455 |
+
else:
|
| 456 |
+
return {name: out[name] for name in names}
|
| 457 |
+
|
| 458 |
+
def du(self, path, total=True, maxdepth=None, **kwargs):
|
| 459 |
+
"""Space used by files within a path
|
| 460 |
+
|
| 461 |
+
Parameters
|
| 462 |
+
----------
|
| 463 |
+
path: str
|
| 464 |
+
total: bool
|
| 465 |
+
whether to sum all the file sizes
|
| 466 |
+
maxdepth: int or None
|
| 467 |
+
maximum number of directory levels to descend, None for unlimited.
|
| 468 |
+
kwargs: passed to ``ls``
|
| 469 |
+
|
| 470 |
+
Returns
|
| 471 |
+
-------
|
| 472 |
+
Dict of {fn: size} if total=False, or int otherwise, where numbers
|
| 473 |
+
refer to bytes used.
|
| 474 |
+
"""
|
| 475 |
+
sizes = {}
|
| 476 |
+
for f in self.find(path, maxdepth=maxdepth, **kwargs):
|
| 477 |
+
info = self.info(f)
|
| 478 |
+
sizes[info["name"]] = info["size"]
|
| 479 |
+
if total:
|
| 480 |
+
return sum(sizes.values())
|
| 481 |
+
else:
|
| 482 |
+
return sizes
|
| 483 |
+
|
| 484 |
+
def glob(self, path, **kwargs):
|
| 485 |
+
"""
|
| 486 |
+
Find files by glob-matching.
|
| 487 |
+
|
| 488 |
+
If the path ends with '/' and does not contain "*", it is essentially
|
| 489 |
+
the same as ``ls(path)``, returning only files.
|
| 490 |
+
|
| 491 |
+
We support ``"**"``,
|
| 492 |
+
``"?"`` and ``"[..]"``. We do not support ^ for pattern negation.
|
| 493 |
+
|
| 494 |
+
Search path names that contain embedded characters special to this
|
| 495 |
+
implementation of glob may not produce expected results;
|
| 496 |
+
e.g., 'foo/bar/*starredfilename*'.
|
| 497 |
+
|
| 498 |
+
kwargs are passed to ``ls``.
|
| 499 |
+
"""
|
| 500 |
+
import re
|
| 501 |
+
|
| 502 |
+
ends = path.endswith("/")
|
| 503 |
+
path = self._strip_protocol(path)
|
| 504 |
+
indstar = path.find("*") if path.find("*") >= 0 else len(path)
|
| 505 |
+
indques = path.find("?") if path.find("?") >= 0 else len(path)
|
| 506 |
+
indbrace = path.find("[") if path.find("[") >= 0 else len(path)
|
| 507 |
+
|
| 508 |
+
ind = min(indstar, indques, indbrace)
|
| 509 |
+
|
| 510 |
+
detail = kwargs.pop("detail", False)
|
| 511 |
+
|
| 512 |
+
if not has_magic(path):
|
| 513 |
+
root = path
|
| 514 |
+
depth = 1
|
| 515 |
+
if ends:
|
| 516 |
+
path += "/*"
|
| 517 |
+
elif self.exists(path):
|
| 518 |
+
if not detail:
|
| 519 |
+
return [path]
|
| 520 |
+
else:
|
| 521 |
+
return {path: self.info(path)}
|
| 522 |
+
else:
|
| 523 |
+
if not detail:
|
| 524 |
+
return [] # glob of non-existent returns empty
|
| 525 |
+
else:
|
| 526 |
+
return {}
|
| 527 |
+
elif "/" in path[:ind]:
|
| 528 |
+
ind2 = path[:ind].rindex("/")
|
| 529 |
+
root = path[: ind2 + 1]
|
| 530 |
+
depth = None if "**" in path else path[ind2 + 1 :].count("/") + 1
|
| 531 |
+
else:
|
| 532 |
+
root = ""
|
| 533 |
+
depth = None if "**" in path else path[ind + 1 :].count("/") + 1
|
| 534 |
+
|
| 535 |
+
allpaths = self.find(root, maxdepth=depth, withdirs=True, detail=True, **kwargs)
|
| 536 |
+
# Escape characters special to python regex, leaving our supported
|
| 537 |
+
# special characters in place.
|
| 538 |
+
# See https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
|
| 539 |
+
# for shell globbing details.
|
| 540 |
+
pattern = (
|
| 541 |
+
"^"
|
| 542 |
+
+ (
|
| 543 |
+
path.replace("\\", r"\\")
|
| 544 |
+
.replace(".", r"\.")
|
| 545 |
+
.replace("+", r"\+")
|
| 546 |
+
.replace("//", "/")
|
| 547 |
+
.replace("(", r"\(")
|
| 548 |
+
.replace(")", r"\)")
|
| 549 |
+
.replace("|", r"\|")
|
| 550 |
+
.replace("^", r"\^")
|
| 551 |
+
.replace("$", r"\$")
|
| 552 |
+
.replace("{", r"\{")
|
| 553 |
+
.replace("}", r"\}")
|
| 554 |
+
.rstrip("/")
|
| 555 |
+
.replace("?", ".")
|
| 556 |
+
)
|
| 557 |
+
+ "$"
|
| 558 |
+
)
|
| 559 |
+
pattern = re.sub("[*]{2}", "=PLACEHOLDER=", pattern)
|
| 560 |
+
pattern = re.sub("[*]", "[^/]*", pattern)
|
| 561 |
+
pattern = re.compile(pattern.replace("=PLACEHOLDER=", ".*"))
|
| 562 |
+
out = {
|
| 563 |
+
p: allpaths[p]
|
| 564 |
+
for p in sorted(allpaths)
|
| 565 |
+
if pattern.match(p.replace("//", "/").rstrip("/"))
|
| 566 |
+
}
|
| 567 |
+
if detail:
|
| 568 |
+
return out
|
| 569 |
+
else:
|
| 570 |
+
return list(out)
|
| 571 |
+
|
| 572 |
+
def exists(self, path, **kwargs):
|
| 573 |
+
"""Is there a file at the given path"""
|
| 574 |
+
try:
|
| 575 |
+
self.info(path, **kwargs)
|
| 576 |
+
return True
|
| 577 |
+
except: # noqa: E722
|
| 578 |
+
# any exception allowed bar FileNotFoundError?
|
| 579 |
+
return False
|
| 580 |
+
|
| 581 |
+
def lexists(self, path, **kwargs):
|
| 582 |
+
"""If there is a file at the given path (including
|
| 583 |
+
broken links)"""
|
| 584 |
+
return self.exists(path)
|
| 585 |
+
|
| 586 |
+
def info(self, path, **kwargs):
|
| 587 |
+
"""Give details of entry at path
|
| 588 |
+
|
| 589 |
+
Returns a single dictionary, with exactly the same information as ``ls``
|
| 590 |
+
would with ``detail=True``.
|
| 591 |
+
|
| 592 |
+
The default implementation should calls ls and could be overridden by a
|
| 593 |
+
shortcut. kwargs are passed on to ```ls()``.
|
| 594 |
+
|
| 595 |
+
Some file systems might not be able to measure the file's size, in
|
| 596 |
+
which case, the returned dict will include ``'size': None``.
|
| 597 |
+
|
| 598 |
+
Returns
|
| 599 |
+
-------
|
| 600 |
+
dict with keys: name (full path in the FS), size (in bytes), type (file,
|
| 601 |
+
directory, or something else) and other FS-specific keys.
|
| 602 |
+
"""
|
| 603 |
+
path = self._strip_protocol(path)
|
| 604 |
+
out = self.ls(self._parent(path), detail=True, **kwargs)
|
| 605 |
+
out = [o for o in out if o["name"].rstrip("/") == path]
|
| 606 |
+
if out:
|
| 607 |
+
return out[0]
|
| 608 |
+
out = self.ls(path, detail=True, **kwargs)
|
| 609 |
+
path = path.rstrip("/")
|
| 610 |
+
out1 = [o for o in out if o["name"].rstrip("/") == path]
|
| 611 |
+
if len(out1) == 1:
|
| 612 |
+
if "size" not in out1[0]:
|
| 613 |
+
out1[0]["size"] = None
|
| 614 |
+
return out1[0]
|
| 615 |
+
elif len(out1) > 1 or out:
|
| 616 |
+
return {"name": path, "size": 0, "type": "directory"}
|
| 617 |
+
else:
|
| 618 |
+
raise FileNotFoundError(path)
|
| 619 |
+
|
| 620 |
+
def checksum(self, path):
|
| 621 |
+
"""Unique value for current version of file
|
| 622 |
+
|
| 623 |
+
If the checksum is the same from one moment to another, the contents
|
| 624 |
+
are guaranteed to be the same. If the checksum changes, the contents
|
| 625 |
+
*might* have changed.
|
| 626 |
+
|
| 627 |
+
This should normally be overridden; default will probably capture
|
| 628 |
+
creation/modification timestamp (which would be good) or maybe
|
| 629 |
+
access timestamp (which would be bad)
|
| 630 |
+
"""
|
| 631 |
+
return int(tokenize(self.info(path)), 16)
|
| 632 |
+
|
| 633 |
+
def size(self, path):
|
| 634 |
+
"""Size in bytes of file"""
|
| 635 |
+
return self.info(path).get("size", None)
|
| 636 |
+
|
| 637 |
+
def sizes(self, paths):
|
| 638 |
+
"""Size in bytes of each file in a list of paths"""
|
| 639 |
+
return [self.size(p) for p in paths]
|
| 640 |
+
|
| 641 |
+
def isdir(self, path):
|
| 642 |
+
"""Is this entry directory-like?"""
|
| 643 |
+
try:
|
| 644 |
+
return self.info(path)["type"] == "directory"
|
| 645 |
+
except IOError:
|
| 646 |
+
return False
|
| 647 |
+
|
| 648 |
+
def isfile(self, path):
|
| 649 |
+
"""Is this entry file-like?"""
|
| 650 |
+
try:
|
| 651 |
+
return self.info(path)["type"] == "file"
|
| 652 |
+
except: # noqa: E722
|
| 653 |
+
return False
|
| 654 |
+
|
| 655 |
+
def cat_file(self, path, start=None, end=None, **kwargs):
|
| 656 |
+
"""Get the content of a file
|
| 657 |
+
|
| 658 |
+
Parameters
|
| 659 |
+
----------
|
| 660 |
+
path: URL of file on this filesystems
|
| 661 |
+
start, end: int
|
| 662 |
+
Bytes limits of the read. If negative, backwards from end,
|
| 663 |
+
like usual python slices. Either can be None for start or
|
| 664 |
+
end of file, respectively
|
| 665 |
+
kwargs: passed to ``open()``.
|
| 666 |
+
"""
|
| 667 |
+
# explicitly set buffering off?
|
| 668 |
+
with self.open(path, "rb", **kwargs) as f:
|
| 669 |
+
if start is not None:
|
| 670 |
+
if start >= 0:
|
| 671 |
+
f.seek(start)
|
| 672 |
+
else:
|
| 673 |
+
f.seek(max(0, f.size + start))
|
| 674 |
+
if end is not None:
|
| 675 |
+
if end < 0:
|
| 676 |
+
end = f.size + end
|
| 677 |
+
return f.read(end - f.tell())
|
| 678 |
+
return f.read()
|
| 679 |
+
|
| 680 |
+
def pipe_file(self, path, value, **kwargs):
|
| 681 |
+
"""Set the bytes of given file"""
|
| 682 |
+
with self.open(path, "wb") as f:
|
| 683 |
+
f.write(value)
|
| 684 |
+
|
| 685 |
+
def pipe(self, path, value=None, **kwargs):
|
| 686 |
+
"""Put value into path
|
| 687 |
+
|
| 688 |
+
(counterpart to ``cat``)
|
| 689 |
+
|
| 690 |
+
Parameters
|
| 691 |
+
----------
|
| 692 |
+
path: string or dict(str, bytes)
|
| 693 |
+
If a string, a single remote location to put ``value`` bytes; if a dict,
|
| 694 |
+
a mapping of {path: bytesvalue}.
|
| 695 |
+
value: bytes, optional
|
| 696 |
+
If using a single path, these are the bytes to put there. Ignored if
|
| 697 |
+
``path`` is a dict
|
| 698 |
+
"""
|
| 699 |
+
if isinstance(path, str):
|
| 700 |
+
self.pipe_file(self._strip_protocol(path), value, **kwargs)
|
| 701 |
+
elif isinstance(path, dict):
|
| 702 |
+
for k, v in path.items():
|
| 703 |
+
self.pipe_file(self._strip_protocol(k), v, **kwargs)
|
| 704 |
+
else:
|
| 705 |
+
raise ValueError("path must be str or dict")
|
| 706 |
+
|
| 707 |
+
def cat_ranges(self, paths, starts, ends, max_gap=None, **kwargs):
|
| 708 |
+
if max_gap is not None:
|
| 709 |
+
raise NotImplementedError
|
| 710 |
+
if not isinstance(paths, list):
|
| 711 |
+
raise TypeError
|
| 712 |
+
if not isinstance(starts, list):
|
| 713 |
+
starts = [starts] * len(paths)
|
| 714 |
+
if not isinstance(ends, list):
|
| 715 |
+
ends = [starts] * len(paths)
|
| 716 |
+
if len(starts) != len(paths) or len(ends) != len(paths):
|
| 717 |
+
raise ValueError
|
| 718 |
+
return [self.cat_file(p, s, e) for p, s, e in zip(paths, starts, ends)]
|
| 719 |
+
|
| 720 |
+
def cat(self, path, recursive=False, on_error="raise", **kwargs):
|
| 721 |
+
"""Fetch (potentially multiple) paths' contents
|
| 722 |
+
|
| 723 |
+
Parameters
|
| 724 |
+
----------
|
| 725 |
+
recursive: bool
|
| 726 |
+
If True, assume the path(s) are directories, and get all the
|
| 727 |
+
contained files
|
| 728 |
+
on_error : "raise", "omit", "return"
|
| 729 |
+
If raise, an underlying exception will be raised (converted to KeyError
|
| 730 |
+
if the type is in self.missing_exceptions); if omit, keys with exception
|
| 731 |
+
will simply not be included in the output; if "return", all keys are
|
| 732 |
+
included in the output, but the value will be bytes or an exception
|
| 733 |
+
instance.
|
| 734 |
+
kwargs: passed to cat_file
|
| 735 |
+
|
| 736 |
+
Returns
|
| 737 |
+
-------
|
| 738 |
+
dict of {path: contents} if there are multiple paths
|
| 739 |
+
or the path has been otherwise expanded
|
| 740 |
+
"""
|
| 741 |
+
paths = self.expand_path(path, recursive=recursive)
|
| 742 |
+
if (
|
| 743 |
+
len(paths) > 1
|
| 744 |
+
or isinstance(path, list)
|
| 745 |
+
or paths[0] != self._strip_protocol(path)
|
| 746 |
+
):
|
| 747 |
+
out = {}
|
| 748 |
+
for path in paths:
|
| 749 |
+
try:
|
| 750 |
+
out[path] = self.cat_file(path, **kwargs)
|
| 751 |
+
except Exception as e:
|
| 752 |
+
if on_error == "raise":
|
| 753 |
+
raise
|
| 754 |
+
if on_error == "return":
|
| 755 |
+
out[path] = e
|
| 756 |
+
return out
|
| 757 |
+
else:
|
| 758 |
+
return self.cat_file(paths[0], **kwargs)
|
| 759 |
+
|
| 760 |
+
def get_file(
|
| 761 |
+
self, rpath, lpath, callback=_DEFAULT_CALLBACK, outfile=None, **kwargs
|
| 762 |
+
):
|
| 763 |
+
"""Copy single remote file to local"""
|
| 764 |
+
if isfilelike(lpath):
|
| 765 |
+
outfile = lpath
|
| 766 |
+
else:
|
| 767 |
+
if self.isdir(rpath):
|
| 768 |
+
os.makedirs(lpath, exist_ok=True)
|
| 769 |
+
return None
|
| 770 |
+
|
| 771 |
+
if outfile is None:
|
| 772 |
+
outfile = open(lpath, "wb")
|
| 773 |
+
|
| 774 |
+
with self.open(rpath, "rb", **kwargs) as f1:
|
| 775 |
+
callback.set_size(getattr(f1, "size", None))
|
| 776 |
+
data = True
|
| 777 |
+
while data:
|
| 778 |
+
data = f1.read(self.blocksize)
|
| 779 |
+
segment_len = outfile.write(data)
|
| 780 |
+
callback.relative_update(segment_len)
|
| 781 |
+
if not isfilelike(lpath):
|
| 782 |
+
outfile.close()
|
| 783 |
+
|
| 784 |
+
def get(self, rpath, lpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs):
|
| 785 |
+
"""Copy file(s) to local.
|
| 786 |
+
|
| 787 |
+
Copies a specific file or tree of files (if recursive=True). If lpath
|
| 788 |
+
ends with a "/", it will be assumed to be a directory, and target files
|
| 789 |
+
will go within. Can submit a list of paths, which may be glob-patterns
|
| 790 |
+
and will be expanded.
|
| 791 |
+
|
| 792 |
+
Calls get_file for each source.
|
| 793 |
+
"""
|
| 794 |
+
from .implementations.local import make_path_posix
|
| 795 |
+
|
| 796 |
+
if isinstance(lpath, str):
|
| 797 |
+
lpath = make_path_posix(lpath)
|
| 798 |
+
rpaths = self.expand_path(rpath, recursive=recursive)
|
| 799 |
+
lpaths = other_paths(rpaths, lpath)
|
| 800 |
+
|
| 801 |
+
callback.set_size(len(lpaths))
|
| 802 |
+
for lpath, rpath in callback.wrap(zip(lpaths, rpaths)):
|
| 803 |
+
callback.branch(rpath, lpath, kwargs)
|
| 804 |
+
self.get_file(rpath, lpath, **kwargs)
|
| 805 |
+
|
| 806 |
+
def put_file(self, lpath, rpath, callback=_DEFAULT_CALLBACK, **kwargs):
|
| 807 |
+
"""Copy single file to remote"""
|
| 808 |
+
if os.path.isdir(lpath):
|
| 809 |
+
self.makedirs(rpath, exist_ok=True)
|
| 810 |
+
return None
|
| 811 |
+
|
| 812 |
+
with open(lpath, "rb") as f1:
|
| 813 |
+
size = f1.seek(0, 2)
|
| 814 |
+
callback.set_size(size)
|
| 815 |
+
f1.seek(0)
|
| 816 |
+
|
| 817 |
+
self.mkdirs(self._parent(os.fspath(rpath)), exist_ok=True)
|
| 818 |
+
with self.open(rpath, "wb", **kwargs) as f2:
|
| 819 |
+
while f1.tell() < size:
|
| 820 |
+
data = f1.read(self.blocksize)
|
| 821 |
+
segment_len = f2.write(data)
|
| 822 |
+
callback.relative_update(segment_len)
|
| 823 |
+
|
| 824 |
+
def put(self, lpath, rpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs):
|
| 825 |
+
"""Copy file(s) from local.
|
| 826 |
+
|
| 827 |
+
Copies a specific file or tree of files (if recursive=True). If rpath
|
| 828 |
+
ends with a "/", it will be assumed to be a directory, and target files
|
| 829 |
+
will go within.
|
| 830 |
+
|
| 831 |
+
Calls put_file for each source.
|
| 832 |
+
"""
|
| 833 |
+
from .implementations.local import LocalFileSystem, make_path_posix
|
| 834 |
+
|
| 835 |
+
rpath = (
|
| 836 |
+
self._strip_protocol(rpath)
|
| 837 |
+
if isinstance(rpath, str)
|
| 838 |
+
else [self._strip_protocol(p) for p in rpath]
|
| 839 |
+
)
|
| 840 |
+
if isinstance(lpath, str):
|
| 841 |
+
lpath = make_path_posix(lpath)
|
| 842 |
+
fs = LocalFileSystem()
|
| 843 |
+
lpaths = fs.expand_path(lpath, recursive=recursive)
|
| 844 |
+
rpaths = other_paths(
|
| 845 |
+
lpaths, rpath, exists=isinstance(rpath, str) and self.isdir(rpath)
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
callback.set_size(len(rpaths))
|
| 849 |
+
for lpath, rpath in callback.wrap(zip(lpaths, rpaths)):
|
| 850 |
+
callback.branch(lpath, rpath, kwargs)
|
| 851 |
+
self.put_file(lpath, rpath, **kwargs)
|
| 852 |
+
|
| 853 |
+
def head(self, path, size=1024):
|
| 854 |
+
"""Get the first ``size`` bytes from file"""
|
| 855 |
+
with self.open(path, "rb") as f:
|
| 856 |
+
return f.read(size)
|
| 857 |
+
|
| 858 |
+
def tail(self, path, size=1024):
|
| 859 |
+
"""Get the last ``size`` bytes from file"""
|
| 860 |
+
with self.open(path, "rb") as f:
|
| 861 |
+
f.seek(max(-size, -f.size), 2)
|
| 862 |
+
return f.read()
|
| 863 |
+
|
| 864 |
+
def cp_file(self, path1, path2, **kwargs):
|
| 865 |
+
raise NotImplementedError
|
| 866 |
+
|
| 867 |
+
def copy(self, path1, path2, recursive=False, on_error=None, **kwargs):
|
| 868 |
+
"""Copy within two locations in the filesystem
|
| 869 |
+
|
| 870 |
+
on_error : "raise", "ignore"
|
| 871 |
+
If raise, any not-found exceptions will be raised; if ignore any
|
| 872 |
+
not-found exceptions will cause the path to be skipped; defaults to
|
| 873 |
+
raise unless recursive is true, where the default is ignore
|
| 874 |
+
"""
|
| 875 |
+
if on_error is None and recursive:
|
| 876 |
+
on_error = "ignore"
|
| 877 |
+
elif on_error is None:
|
| 878 |
+
on_error = "raise"
|
| 879 |
+
|
| 880 |
+
paths = self.expand_path(path1, recursive=recursive)
|
| 881 |
+
path2 = other_paths(paths, path2)
|
| 882 |
+
for p1, p2 in zip(paths, path2):
|
| 883 |
+
try:
|
| 884 |
+
self.cp_file(p1, p2, **kwargs)
|
| 885 |
+
except FileNotFoundError:
|
| 886 |
+
if on_error == "raise":
|
| 887 |
+
raise
|
| 888 |
+
|
| 889 |
+
def expand_path(self, path, recursive=False, maxdepth=None):
|
| 890 |
+
"""Turn one or more globs or directories into a list of all matching paths
|
| 891 |
+
to files or directories."""
|
| 892 |
+
if isinstance(path, str):
|
| 893 |
+
out = self.expand_path([path], recursive, maxdepth)
|
| 894 |
+
else:
|
| 895 |
+
# reduce depth on each recursion level unless None or 0
|
| 896 |
+
maxdepth = maxdepth if not maxdepth else maxdepth - 1
|
| 897 |
+
out = set()
|
| 898 |
+
path = [self._strip_protocol(p) for p in path]
|
| 899 |
+
for p in path:
|
| 900 |
+
if has_magic(p):
|
| 901 |
+
bit = set(self.glob(p))
|
| 902 |
+
out |= bit
|
| 903 |
+
if recursive:
|
| 904 |
+
out |= set(
|
| 905 |
+
self.expand_path(
|
| 906 |
+
list(bit), recursive=recursive, maxdepth=maxdepth
|
| 907 |
+
)
|
| 908 |
+
)
|
| 909 |
+
continue
|
| 910 |
+
elif recursive:
|
| 911 |
+
rec = set(self.find(p, maxdepth=maxdepth, withdirs=True))
|
| 912 |
+
out |= rec
|
| 913 |
+
if p not in out and (recursive is False or self.exists(p)):
|
| 914 |
+
# should only check once, for the root
|
| 915 |
+
out.add(p)
|
| 916 |
+
if not out:
|
| 917 |
+
raise FileNotFoundError(path)
|
| 918 |
+
return list(sorted(out))
|
| 919 |
+
|
| 920 |
+
def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs):
|
| 921 |
+
"""Move file(s) from one location to another"""
|
| 922 |
+
self.copy(path1, path2, recursive=recursive, maxdepth=maxdepth)
|
| 923 |
+
self.rm(path1, recursive=recursive)
|
| 924 |
+
|
| 925 |
+
def rm_file(self, path):
|
| 926 |
+
"""Delete a file"""
|
| 927 |
+
self._rm(path)
|
| 928 |
+
|
| 929 |
+
def _rm(self, path):
|
| 930 |
+
"""Delete one file"""
|
| 931 |
+
# this is the old name for the method, prefer rm_file
|
| 932 |
+
raise NotImplementedError
|
| 933 |
+
|
| 934 |
+
def rm(self, path, recursive=False, maxdepth=None):
|
| 935 |
+
"""Delete files.
|
| 936 |
+
|
| 937 |
+
Parameters
|
| 938 |
+
----------
|
| 939 |
+
path: str or list of str
|
| 940 |
+
File(s) to delete.
|
| 941 |
+
recursive: bool
|
| 942 |
+
If file(s) are directories, recursively delete contents and then
|
| 943 |
+
also remove the directory
|
| 944 |
+
maxdepth: int or None
|
| 945 |
+
Depth to pass to walk for finding files to delete, if recursive.
|
| 946 |
+
If None, there will be no limit and infinite recursion may be
|
| 947 |
+
possible.
|
| 948 |
+
"""
|
| 949 |
+
path = self.expand_path(path, recursive=recursive, maxdepth=maxdepth)
|
| 950 |
+
for p in reversed(path):
|
| 951 |
+
self.rm_file(p)
|
| 952 |
+
|
| 953 |
+
@classmethod
|
| 954 |
+
def _parent(cls, path):
|
| 955 |
+
path = cls._strip_protocol(path.rstrip("/"))
|
| 956 |
+
if "/" in path:
|
| 957 |
+
parent = path.rsplit("/", 1)[0].lstrip(cls.root_marker)
|
| 958 |
+
return cls.root_marker + parent
|
| 959 |
+
else:
|
| 960 |
+
return cls.root_marker
|
| 961 |
+
|
| 962 |
+
def _open(
|
| 963 |
+
self,
|
| 964 |
+
path,
|
| 965 |
+
mode="rb",
|
| 966 |
+
block_size=None,
|
| 967 |
+
autocommit=True,
|
| 968 |
+
cache_options=None,
|
| 969 |
+
**kwargs,
|
| 970 |
+
):
|
| 971 |
+
"""Return raw bytes-mode file-like from the file-system"""
|
| 972 |
+
return AbstractBufferedFile(
|
| 973 |
+
self,
|
| 974 |
+
path,
|
| 975 |
+
mode,
|
| 976 |
+
block_size,
|
| 977 |
+
autocommit,
|
| 978 |
+
cache_options=cache_options,
|
| 979 |
+
**kwargs,
|
| 980 |
+
)
|
| 981 |
+
|
| 982 |
+
def open(
|
| 983 |
+
self,
|
| 984 |
+
path,
|
| 985 |
+
mode="rb",
|
| 986 |
+
block_size=None,
|
| 987 |
+
cache_options=None,
|
| 988 |
+
compression=None,
|
| 989 |
+
**kwargs,
|
| 990 |
+
):
|
| 991 |
+
"""
|
| 992 |
+
Return a file-like object from the filesystem
|
| 993 |
+
|
| 994 |
+
The resultant instance must function correctly in a context ``with``
|
| 995 |
+
block.
|
| 996 |
+
|
| 997 |
+
Parameters
|
| 998 |
+
----------
|
| 999 |
+
path: str
|
| 1000 |
+
Target file
|
| 1001 |
+
mode: str like 'rb', 'w'
|
| 1002 |
+
See builtin ``open()``
|
| 1003 |
+
block_size: int
|
| 1004 |
+
Some indication of buffering - this is a value in bytes
|
| 1005 |
+
cache_options : dict, optional
|
| 1006 |
+
Extra arguments to pass through to the cache.
|
| 1007 |
+
compression: string or None
|
| 1008 |
+
If given, open file using compression codec. Can either be a compression
|
| 1009 |
+
name (a key in ``fsspec.compression.compr``) or "infer" to guess the
|
| 1010 |
+
compression from the filename suffix.
|
| 1011 |
+
encoding, errors, newline: passed on to TextIOWrapper for text mode
|
| 1012 |
+
"""
|
| 1013 |
+
import io
|
| 1014 |
+
|
| 1015 |
+
path = self._strip_protocol(path)
|
| 1016 |
+
if "b" not in mode:
|
| 1017 |
+
mode = mode.replace("t", "") + "b"
|
| 1018 |
+
|
| 1019 |
+
text_kwargs = {
|
| 1020 |
+
k: kwargs.pop(k)
|
| 1021 |
+
for k in ["encoding", "errors", "newline"]
|
| 1022 |
+
if k in kwargs
|
| 1023 |
+
}
|
| 1024 |
+
return io.TextIOWrapper(
|
| 1025 |
+
self.open(
|
| 1026 |
+
path,
|
| 1027 |
+
mode,
|
| 1028 |
+
block_size=block_size,
|
| 1029 |
+
cache_options=cache_options,
|
| 1030 |
+
compression=compression,
|
| 1031 |
+
**kwargs,
|
| 1032 |
+
),
|
| 1033 |
+
**text_kwargs,
|
| 1034 |
+
)
|
| 1035 |
+
else:
|
| 1036 |
+
ac = kwargs.pop("autocommit", not self._intrans)
|
| 1037 |
+
f = self._open(
|
| 1038 |
+
path,
|
| 1039 |
+
mode=mode,
|
| 1040 |
+
block_size=block_size,
|
| 1041 |
+
autocommit=ac,
|
| 1042 |
+
cache_options=cache_options,
|
| 1043 |
+
**kwargs,
|
| 1044 |
+
)
|
| 1045 |
+
if compression is not None:
|
| 1046 |
+
from fsspec.compression import compr
|
| 1047 |
+
from fsspec.core import get_compression
|
| 1048 |
+
|
| 1049 |
+
compression = get_compression(path, compression)
|
| 1050 |
+
compress = compr[compression]
|
| 1051 |
+
f = compress(f, mode=mode[0])
|
| 1052 |
+
|
| 1053 |
+
if not ac and "r" not in mode:
|
| 1054 |
+
self.transaction.files.append(f)
|
| 1055 |
+
return f
|
| 1056 |
+
|
| 1057 |
+
def touch(self, path, truncate=True, **kwargs):
|
| 1058 |
+
"""Create empty file, or update timestamp
|
| 1059 |
+
|
| 1060 |
+
Parameters
|
| 1061 |
+
----------
|
| 1062 |
+
path: str
|
| 1063 |
+
file location
|
| 1064 |
+
truncate: bool
|
| 1065 |
+
If True, always set file size to 0; if False, update timestamp and
|
| 1066 |
+
leave file unchanged, if backend allows this
|
| 1067 |
+
"""
|
| 1068 |
+
if truncate or not self.exists(path):
|
| 1069 |
+
with self.open(path, "wb", **kwargs):
|
| 1070 |
+
pass
|
| 1071 |
+
else:
|
| 1072 |
+
raise NotImplementedError # update timestamp, if possible
|
| 1073 |
+
|
| 1074 |
+
def ukey(self, path):
|
| 1075 |
+
"""Hash of file properties, to tell if it has changed"""
|
| 1076 |
+
return sha256(str(self.info(path)).encode()).hexdigest()
|
| 1077 |
+
|
| 1078 |
+
def read_block(self, fn, offset, length, delimiter=None):
|
| 1079 |
+
"""Read a block of bytes from
|
| 1080 |
+
|
| 1081 |
+
Starting at ``offset`` of the file, read ``length`` bytes. If
|
| 1082 |
+
``delimiter`` is set then we ensure that the read starts and stops at
|
| 1083 |
+
delimiter boundaries that follow the locations ``offset`` and ``offset
|
| 1084 |
+
+ length``. If ``offset`` is zero then we start at zero. The
|
| 1085 |
+
bytestring returned WILL include the end delimiter string.
|
| 1086 |
+
|
| 1087 |
+
If offset+length is beyond the eof, reads to eof.
|
| 1088 |
+
|
| 1089 |
+
Parameters
|
| 1090 |
+
----------
|
| 1091 |
+
fn: string
|
| 1092 |
+
Path to filename
|
| 1093 |
+
offset: int
|
| 1094 |
+
Byte offset to start read
|
| 1095 |
+
length: int
|
| 1096 |
+
Number of bytes to read
|
| 1097 |
+
delimiter: bytes (optional)
|
| 1098 |
+
Ensure reading starts and stops at delimiter bytestring
|
| 1099 |
+
|
| 1100 |
+
Examples
|
| 1101 |
+
--------
|
| 1102 |
+
>>> fs.read_block('data/file.csv', 0, 13) # doctest: +SKIP
|
| 1103 |
+
b'Alice, 100\\nBo'
|
| 1104 |
+
>>> fs.read_block('data/file.csv', 0, 13, delimiter=b'\\n') # doctest: +SKIP
|
| 1105 |
+
b'Alice, 100\\nBob, 200\\n'
|
| 1106 |
+
|
| 1107 |
+
Use ``length=None`` to read to the end of the file.
|
| 1108 |
+
>>> fs.read_block('data/file.csv', 0, None, delimiter=b'\\n') # doctest: +SKIP
|
| 1109 |
+
b'Alice, 100\\nBob, 200\\nCharlie, 300'
|
| 1110 |
+
|
| 1111 |
+
See Also
|
| 1112 |
+
--------
|
| 1113 |
+
utils.read_block
|
| 1114 |
+
"""
|
| 1115 |
+
with self.open(fn, "rb") as f:
|
| 1116 |
+
size = f.size
|
| 1117 |
+
if length is None:
|
| 1118 |
+
length = size
|
| 1119 |
+
if size is not None and offset + length > size:
|
| 1120 |
+
length = size - offset
|
| 1121 |
+
return read_block(f, offset, length, delimiter)
|
| 1122 |
+
|
| 1123 |
+
def to_json(self):
|
| 1124 |
+
"""
|
| 1125 |
+
JSON representation of this filesystem instance
|
| 1126 |
+
|
| 1127 |
+
Returns
|
| 1128 |
+
-------
|
| 1129 |
+
str: JSON structure with keys cls (the python location of this class),
|
| 1130 |
+
protocol (text name of this class's protocol, first one in case of
|
| 1131 |
+
multiple), args (positional args, usually empty), and all other
|
| 1132 |
+
kwargs as their own keys.
|
| 1133 |
+
"""
|
| 1134 |
+
import json
|
| 1135 |
+
|
| 1136 |
+
cls = type(self)
|
| 1137 |
+
cls = ".".join((cls.__module__, cls.__name__))
|
| 1138 |
+
proto = (
|
| 1139 |
+
self.protocol[0]
|
| 1140 |
+
if isinstance(self.protocol, (tuple, list))
|
| 1141 |
+
else self.protocol
|
| 1142 |
+
)
|
| 1143 |
+
return json.dumps(
|
| 1144 |
+
dict(
|
| 1145 |
+
**{"cls": cls, "protocol": proto, "args": self.storage_args},
|
| 1146 |
+
**self.storage_options,
|
| 1147 |
+
)
|
| 1148 |
+
)
|
| 1149 |
+
|
| 1150 |
+
@staticmethod
|
| 1151 |
+
def from_json(blob):
|
| 1152 |
+
"""
|
| 1153 |
+
Recreate a filesystem instance from JSON representation
|
| 1154 |
+
|
| 1155 |
+
See ``.to_json()`` for the expected structure of the input
|
| 1156 |
+
|
| 1157 |
+
Parameters
|
| 1158 |
+
----------
|
| 1159 |
+
blob: str
|
| 1160 |
+
|
| 1161 |
+
Returns
|
| 1162 |
+
-------
|
| 1163 |
+
file system instance, not necessarily of this particular class.
|
| 1164 |
+
"""
|
| 1165 |
+
import json
|
| 1166 |
+
|
| 1167 |
+
from .registry import _import_class, get_filesystem_class
|
| 1168 |
+
|
| 1169 |
+
dic = json.loads(blob)
|
| 1170 |
+
protocol = dic.pop("protocol")
|
| 1171 |
+
try:
|
| 1172 |
+
cls = _import_class(dic.pop("cls"))
|
| 1173 |
+
except (ImportError, ValueError, RuntimeError, KeyError):
|
| 1174 |
+
cls = get_filesystem_class(protocol)
|
| 1175 |
+
return cls(*dic.pop("args", ()), **dic)
|
| 1176 |
+
|
| 1177 |
+
def _get_pyarrow_filesystem(self):
|
| 1178 |
+
"""
|
| 1179 |
+
Make a version of the FS instance which will be acceptable to pyarrow
|
| 1180 |
+
"""
|
| 1181 |
+
# all instances already also derive from pyarrow
|
| 1182 |
+
return self
|
| 1183 |
+
|
| 1184 |
+
def get_mapper(self, root="", check=False, create=False, missing_exceptions=None):
|
| 1185 |
+
"""Create key/value store based on this file-system
|
| 1186 |
+
|
| 1187 |
+
Makes a MutableMapping interface to the FS at the given root path.
|
| 1188 |
+
See ``fsspec.mapping.FSMap`` for further details.
|
| 1189 |
+
"""
|
| 1190 |
+
from .mapping import FSMap
|
| 1191 |
+
|
| 1192 |
+
return FSMap(
|
| 1193 |
+
root,
|
| 1194 |
+
self,
|
| 1195 |
+
check=check,
|
| 1196 |
+
create=create,
|
| 1197 |
+
missing_exceptions=missing_exceptions,
|
| 1198 |
+
)
|
| 1199 |
+
|
| 1200 |
+
@classmethod
|
| 1201 |
+
def clear_instance_cache(cls):
|
| 1202 |
+
"""
|
| 1203 |
+
Clear the cache of filesystem instances.
|
| 1204 |
+
|
| 1205 |
+
Notes
|
| 1206 |
+
-----
|
| 1207 |
+
Unless overridden by setting the ``cachable`` class attribute to False,
|
| 1208 |
+
the filesystem class stores a reference to newly created instances. This
|
| 1209 |
+
prevents Python's normal rules around garbage collection from working,
|
| 1210 |
+
since the instances refcount will not drop to zero until
|
| 1211 |
+
``clear_instance_cache`` is called.
|
| 1212 |
+
"""
|
| 1213 |
+
cls._cache.clear()
|
| 1214 |
+
|
| 1215 |
+
def created(self, path):
|
| 1216 |
+
"""Return the created timestamp of a file as a datetime.datetime"""
|
| 1217 |
+
raise NotImplementedError
|
| 1218 |
+
|
| 1219 |
+
def modified(self, path):
|
| 1220 |
+
"""Return the modified timestamp of a file as a datetime.datetime"""
|
| 1221 |
+
raise NotImplementedError
|
| 1222 |
+
|
| 1223 |
+
# ------------------------------------------------------------------------
|
| 1224 |
+
# Aliases
|
| 1225 |
+
|
| 1226 |
+
def makedir(self, path, create_parents=True, **kwargs):
|
| 1227 |
+
"""Alias of `AbstractFileSystem.mkdir`."""
|
| 1228 |
+
return self.mkdir(path, create_parents=create_parents, **kwargs)
|
| 1229 |
+
|
| 1230 |
+
def mkdirs(self, path, exist_ok=False):
|
| 1231 |
+
"""Alias of `AbstractFileSystem.makedirs`."""
|
| 1232 |
+
return self.makedirs(path, exist_ok=exist_ok)
|
| 1233 |
+
|
| 1234 |
+
def listdir(self, path, detail=True, **kwargs):
|
| 1235 |
+
"""Alias of `AbstractFileSystem.ls`."""
|
| 1236 |
+
return self.ls(path, detail=detail, **kwargs)
|
| 1237 |
+
|
| 1238 |
+
def cp(self, path1, path2, **kwargs):
|
| 1239 |
+
"""Alias of `AbstractFileSystem.copy`."""
|
| 1240 |
+
return self.copy(path1, path2, **kwargs)
|
| 1241 |
+
|
| 1242 |
+
def move(self, path1, path2, **kwargs):
|
| 1243 |
+
"""Alias of `AbstractFileSystem.mv`."""
|
| 1244 |
+
return self.mv(path1, path2, **kwargs)
|
| 1245 |
+
|
| 1246 |
+
def stat(self, path, **kwargs):
|
| 1247 |
+
"""Alias of `AbstractFileSystem.info`."""
|
| 1248 |
+
return self.info(path, **kwargs)
|
| 1249 |
+
|
| 1250 |
+
def disk_usage(self, path, total=True, maxdepth=None, **kwargs):
|
| 1251 |
+
"""Alias of `AbstractFileSystem.du`."""
|
| 1252 |
+
return self.du(path, total=total, maxdepth=maxdepth, **kwargs)
|
| 1253 |
+
|
| 1254 |
+
def rename(self, path1, path2, **kwargs):
|
| 1255 |
+
"""Alias of `AbstractFileSystem.mv`."""
|
| 1256 |
+
return self.mv(path1, path2, **kwargs)
|
| 1257 |
+
|
| 1258 |
+
def delete(self, path, recursive=False, maxdepth=None):
|
| 1259 |
+
"""Alias of `AbstractFileSystem.rm`."""
|
| 1260 |
+
return self.rm(path, recursive=recursive, maxdepth=maxdepth)
|
| 1261 |
+
|
| 1262 |
+
def upload(self, lpath, rpath, recursive=False, **kwargs):
|
| 1263 |
+
"""Alias of `AbstractFileSystem.put`."""
|
| 1264 |
+
return self.put(lpath, rpath, recursive=recursive, **kwargs)
|
| 1265 |
+
|
| 1266 |
+
def download(self, rpath, lpath, recursive=False, **kwargs):
|
| 1267 |
+
"""Alias of `AbstractFileSystem.get`."""
|
| 1268 |
+
return self.get(rpath, lpath, recursive=recursive, **kwargs)
|
| 1269 |
+
|
| 1270 |
+
def sign(self, path, expiration=100, **kwargs):
|
| 1271 |
+
"""Create a signed URL representing the given path
|
| 1272 |
+
|
| 1273 |
+
Some implementations allow temporary URLs to be generated, as a
|
| 1274 |
+
way of delegating credentials.
|
| 1275 |
+
|
| 1276 |
+
Parameters
|
| 1277 |
+
----------
|
| 1278 |
+
path : str
|
| 1279 |
+
The path on the filesystem
|
| 1280 |
+
expiration : int
|
| 1281 |
+
Number of seconds to enable the URL for (if supported)
|
| 1282 |
+
|
| 1283 |
+
Returns
|
| 1284 |
+
-------
|
| 1285 |
+
URL : str
|
| 1286 |
+
The signed URL
|
| 1287 |
+
|
| 1288 |
+
Raises
|
| 1289 |
+
------
|
| 1290 |
+
NotImplementedError : if method is not implemented for a filesystem
|
| 1291 |
+
"""
|
| 1292 |
+
raise NotImplementedError("Sign is not implemented for this filesystem")
|
| 1293 |
+
|
| 1294 |
+
def _isfilestore(self):
|
| 1295 |
+
# Originally inherited from pyarrow DaskFileSystem. Keeping this
|
| 1296 |
+
# here for backwards compatibility as long as pyarrow uses its
|
| 1297 |
+
# legacy fsspec-compatible filesystems and thus accepts fsspec
|
| 1298 |
+
# filesystems as well
|
| 1299 |
+
return False
|
| 1300 |
+
|
| 1301 |
+
|
| 1302 |
+
class AbstractBufferedFile(io.IOBase):
|
| 1303 |
+
"""Convenient class to derive from to provide buffering
|
| 1304 |
+
|
| 1305 |
+
In the case that the backend does not provide a pythonic file-like object
|
| 1306 |
+
already, this class contains much of the logic to build one. The only
|
| 1307 |
+
methods that need to be overridden are ``_upload_chunk``,
|
| 1308 |
+
``_initiate_upload`` and ``_fetch_range``.
|
| 1309 |
+
"""
|
| 1310 |
+
|
| 1311 |
+
DEFAULT_BLOCK_SIZE = 5 * 2**20
|
| 1312 |
+
_details = None
|
| 1313 |
+
|
| 1314 |
+
def __init__(
|
| 1315 |
+
self,
|
| 1316 |
+
fs,
|
| 1317 |
+
path,
|
| 1318 |
+
mode="rb",
|
| 1319 |
+
block_size="default",
|
| 1320 |
+
autocommit=True,
|
| 1321 |
+
cache_type="readahead",
|
| 1322 |
+
cache_options=None,
|
| 1323 |
+
size=None,
|
| 1324 |
+
**kwargs,
|
| 1325 |
+
):
|
| 1326 |
+
"""
|
| 1327 |
+
Template for files with buffered reading and writing
|
| 1328 |
+
|
| 1329 |
+
Parameters
|
| 1330 |
+
----------
|
| 1331 |
+
fs: instance of FileSystem
|
| 1332 |
+
path: str
|
| 1333 |
+
location in file-system
|
| 1334 |
+
mode: str
|
| 1335 |
+
Normal file modes. Currently only 'wb', 'ab' or 'rb'. Some file
|
| 1336 |
+
systems may be read-only, and some may not support append.
|
| 1337 |
+
block_size: int
|
| 1338 |
+
Buffer size for reading or writing, 'default' for class default
|
| 1339 |
+
autocommit: bool
|
| 1340 |
+
Whether to write to final destination; may only impact what
|
| 1341 |
+
happens when file is being closed.
|
| 1342 |
+
cache_type: {"readahead", "none", "mmap", "bytes"}, default "readahead"
|
| 1343 |
+
Caching policy in read mode. See the definitions in ``core``.
|
| 1344 |
+
cache_options : dict
|
| 1345 |
+
Additional options passed to the constructor for the cache specified
|
| 1346 |
+
by `cache_type`.
|
| 1347 |
+
size: int
|
| 1348 |
+
If given and in read mode, suppressed having to look up the file size
|
| 1349 |
+
kwargs:
|
| 1350 |
+
Gets stored as self.kwargs
|
| 1351 |
+
"""
|
| 1352 |
+
from .core import caches
|
| 1353 |
+
|
| 1354 |
+
self.path = path
|
| 1355 |
+
self.fs = fs
|
| 1356 |
+
self.mode = mode
|
| 1357 |
+
self.blocksize = (
|
| 1358 |
+
self.DEFAULT_BLOCK_SIZE if block_size in ["default", None] else block_size
|
| 1359 |
+
)
|
| 1360 |
+
self.loc = 0
|
| 1361 |
+
self.autocommit = autocommit
|
| 1362 |
+
self.end = None
|
| 1363 |
+
self.start = None
|
| 1364 |
+
self.closed = False
|
| 1365 |
+
|
| 1366 |
+
if cache_options is None:
|
| 1367 |
+
cache_options = {}
|
| 1368 |
+
|
| 1369 |
+
if "trim" in kwargs:
|
| 1370 |
+
warnings.warn(
|
| 1371 |
+
"Passing 'trim' to control the cache behavior has been deprecated. "
|
| 1372 |
+
"Specify it within the 'cache_options' argument instead.",
|
| 1373 |
+
FutureWarning,
|
| 1374 |
+
)
|
| 1375 |
+
cache_options["trim"] = kwargs.pop("trim")
|
| 1376 |
+
|
| 1377 |
+
self.kwargs = kwargs
|
| 1378 |
+
|
| 1379 |
+
if mode not in {"ab", "rb", "wb"}:
|
| 1380 |
+
raise NotImplementedError("File mode not supported")
|
| 1381 |
+
if mode == "rb":
|
| 1382 |
+
if size is not None:
|
| 1383 |
+
self.size = size
|
| 1384 |
+
else:
|
| 1385 |
+
self.size = self.details["size"]
|
| 1386 |
+
self.cache = caches[cache_type](
|
| 1387 |
+
self.blocksize, self._fetch_range, self.size, **cache_options
|
| 1388 |
+
)
|
| 1389 |
+
else:
|
| 1390 |
+
self.buffer = io.BytesIO()
|
| 1391 |
+
self.offset = None
|
| 1392 |
+
self.forced = False
|
| 1393 |
+
self.location = None
|
| 1394 |
+
|
| 1395 |
+
@property
|
| 1396 |
+
def details(self):
|
| 1397 |
+
if self._details is None:
|
| 1398 |
+
self._details = self.fs.info(self.path)
|
| 1399 |
+
return self._details
|
| 1400 |
+
|
| 1401 |
+
@details.setter
|
| 1402 |
+
def details(self, value):
|
| 1403 |
+
self._details = value
|
| 1404 |
+
self.size = value["size"]
|
| 1405 |
+
|
| 1406 |
+
@property
|
| 1407 |
+
def full_name(self):
|
| 1408 |
+
return _unstrip_protocol(self.path, self.fs)
|
| 1409 |
+
|
| 1410 |
+
@property
|
| 1411 |
+
def closed(self):
|
| 1412 |
+
# get around this attr being read-only in IOBase
|
| 1413 |
+
# use getattr here, since this can be called during del
|
| 1414 |
+
return getattr(self, "_closed", True)
|
| 1415 |
+
|
| 1416 |
+
@closed.setter
|
| 1417 |
+
def closed(self, c):
|
| 1418 |
+
self._closed = c
|
| 1419 |
+
|
| 1420 |
+
def __hash__(self):
|
| 1421 |
+
if "w" in self.mode:
|
| 1422 |
+
return id(self)
|
| 1423 |
+
else:
|
| 1424 |
+
return int(tokenize(self.details), 16)
|
| 1425 |
+
|
| 1426 |
+
def __eq__(self, other):
|
| 1427 |
+
"""Files are equal if they have the same checksum, only in read mode"""
|
| 1428 |
+
return self.mode == "rb" and other.mode == "rb" and hash(self) == hash(other)
|
| 1429 |
+
|
| 1430 |
+
def commit(self):
|
| 1431 |
+
"""Move from temp to final destination"""
|
| 1432 |
+
|
| 1433 |
+
def discard(self):
|
| 1434 |
+
"""Throw away temporary file"""
|
| 1435 |
+
|
| 1436 |
+
def info(self):
|
| 1437 |
+
"""File information about this path"""
|
| 1438 |
+
if "r" in self.mode:
|
| 1439 |
+
return self.details
|
| 1440 |
+
else:
|
| 1441 |
+
raise ValueError("Info not available while writing")
|
| 1442 |
+
|
| 1443 |
+
def tell(self):
|
| 1444 |
+
"""Current file location"""
|
| 1445 |
+
return self.loc
|
| 1446 |
+
|
| 1447 |
+
def seek(self, loc, whence=0):
|
| 1448 |
+
"""Set current file location
|
| 1449 |
+
|
| 1450 |
+
Parameters
|
| 1451 |
+
----------
|
| 1452 |
+
loc: int
|
| 1453 |
+
byte location
|
| 1454 |
+
whence: {0, 1, 2}
|
| 1455 |
+
from start of file, current location or end of file, resp.
|
| 1456 |
+
"""
|
| 1457 |
+
loc = int(loc)
|
| 1458 |
+
if not self.mode == "rb":
|
| 1459 |
+
raise OSError(ESPIPE, "Seek only available in read mode")
|
| 1460 |
+
if whence == 0:
|
| 1461 |
+
nloc = loc
|
| 1462 |
+
elif whence == 1:
|
| 1463 |
+
nloc = self.loc + loc
|
| 1464 |
+
elif whence == 2:
|
| 1465 |
+
nloc = self.size + loc
|
| 1466 |
+
else:
|
| 1467 |
+
raise ValueError("invalid whence (%s, should be 0, 1 or 2)" % whence)
|
| 1468 |
+
if nloc < 0:
|
| 1469 |
+
raise ValueError("Seek before start of file")
|
| 1470 |
+
self.loc = nloc
|
| 1471 |
+
return self.loc
|
| 1472 |
+
|
| 1473 |
+
def write(self, data):
|
| 1474 |
+
"""
|
| 1475 |
+
Write data to buffer.
|
| 1476 |
+
|
| 1477 |
+
Buffer only sent on flush() or if buffer is greater than
|
| 1478 |
+
or equal to blocksize.
|
| 1479 |
+
|
| 1480 |
+
Parameters
|
| 1481 |
+
----------
|
| 1482 |
+
data: bytes
|
| 1483 |
+
Set of bytes to be written.
|
| 1484 |
+
"""
|
| 1485 |
+
if self.mode not in {"wb", "ab"}:
|
| 1486 |
+
raise ValueError("File not in write mode")
|
| 1487 |
+
if self.closed:
|
| 1488 |
+
raise ValueError("I/O operation on closed file.")
|
| 1489 |
+
if self.forced:
|
| 1490 |
+
raise ValueError("This file has been force-flushed, can only close")
|
| 1491 |
+
out = self.buffer.write(data)
|
| 1492 |
+
self.loc += out
|
| 1493 |
+
if self.buffer.tell() >= self.blocksize:
|
| 1494 |
+
self.flush()
|
| 1495 |
+
return out
|
| 1496 |
+
|
| 1497 |
+
def flush(self, force=False):
|
| 1498 |
+
"""
|
| 1499 |
+
Write buffered data to backend store.
|
| 1500 |
+
|
| 1501 |
+
Writes the current buffer, if it is larger than the block-size, or if
|
| 1502 |
+
the file is being closed.
|
| 1503 |
+
|
| 1504 |
+
Parameters
|
| 1505 |
+
----------
|
| 1506 |
+
force: bool
|
| 1507 |
+
When closing, write the last block even if it is smaller than
|
| 1508 |
+
blocks are allowed to be. Disallows further writing to this file.
|
| 1509 |
+
"""
|
| 1510 |
+
|
| 1511 |
+
if self.closed:
|
| 1512 |
+
raise ValueError("Flush on closed file")
|
| 1513 |
+
if force and self.forced:
|
| 1514 |
+
raise ValueError("Force flush cannot be called more than once")
|
| 1515 |
+
if force:
|
| 1516 |
+
self.forced = True
|
| 1517 |
+
|
| 1518 |
+
if self.mode not in {"wb", "ab"}:
|
| 1519 |
+
# no-op to flush on read-mode
|
| 1520 |
+
return
|
| 1521 |
+
|
| 1522 |
+
if not force and self.buffer.tell() < self.blocksize:
|
| 1523 |
+
# Defer write on small block
|
| 1524 |
+
return
|
| 1525 |
+
|
| 1526 |
+
if self.offset is None:
|
| 1527 |
+
# Initialize a multipart upload
|
| 1528 |
+
self.offset = 0
|
| 1529 |
+
try:
|
| 1530 |
+
self._initiate_upload()
|
| 1531 |
+
except: # noqa: E722
|
| 1532 |
+
self.closed = True
|
| 1533 |
+
raise
|
| 1534 |
+
|
| 1535 |
+
if self._upload_chunk(final=force) is not False:
|
| 1536 |
+
self.offset += self.buffer.seek(0, 2)
|
| 1537 |
+
self.buffer = io.BytesIO()
|
| 1538 |
+
|
| 1539 |
+
def _upload_chunk(self, final=False):
|
| 1540 |
+
"""Write one part of a multi-block file upload
|
| 1541 |
+
|
| 1542 |
+
Parameters
|
| 1543 |
+
==========
|
| 1544 |
+
final: bool
|
| 1545 |
+
This is the last block, so should complete file, if
|
| 1546 |
+
self.autocommit is True.
|
| 1547 |
+
"""
|
| 1548 |
+
# may not yet have been initialized, may need to call _initialize_upload
|
| 1549 |
+
|
| 1550 |
+
def _initiate_upload(self):
|
| 1551 |
+
"""Create remote file/upload"""
|
| 1552 |
+
pass
|
| 1553 |
+
|
| 1554 |
+
def _fetch_range(self, start, end):
|
| 1555 |
+
"""Get the specified set of bytes from remote"""
|
| 1556 |
+
raise NotImplementedError
|
| 1557 |
+
|
| 1558 |
+
def read(self, length=-1):
|
| 1559 |
+
"""
|
| 1560 |
+
Return data from cache, or fetch pieces as necessary
|
| 1561 |
+
|
| 1562 |
+
Parameters
|
| 1563 |
+
----------
|
| 1564 |
+
length: int (-1)
|
| 1565 |
+
Number of bytes to read; if <0, all remaining bytes.
|
| 1566 |
+
"""
|
| 1567 |
+
length = -1 if length is None else int(length)
|
| 1568 |
+
if self.mode != "rb":
|
| 1569 |
+
raise ValueError("File not in read mode")
|
| 1570 |
+
if length < 0:
|
| 1571 |
+
length = self.size - self.loc
|
| 1572 |
+
if self.closed:
|
| 1573 |
+
raise ValueError("I/O operation on closed file.")
|
| 1574 |
+
logger.debug("%s read: %i - %i" % (self, self.loc, self.loc + length))
|
| 1575 |
+
if length == 0:
|
| 1576 |
+
# don't even bother calling fetch
|
| 1577 |
+
return b""
|
| 1578 |
+
out = self.cache._fetch(self.loc, self.loc + length)
|
| 1579 |
+
self.loc += len(out)
|
| 1580 |
+
return out
|
| 1581 |
+
|
| 1582 |
+
def readinto(self, b):
|
| 1583 |
+
"""mirrors builtin file's readinto method
|
| 1584 |
+
|
| 1585 |
+
https://docs.python.org/3/library/io.html#io.RawIOBase.readinto
|
| 1586 |
+
"""
|
| 1587 |
+
out = memoryview(b).cast("B")
|
| 1588 |
+
data = self.read(out.nbytes)
|
| 1589 |
+
out[: len(data)] = data
|
| 1590 |
+
return len(data)
|
| 1591 |
+
|
| 1592 |
+
def readuntil(self, char=b"\n", blocks=None):
|
| 1593 |
+
"""Return data between current position and first occurrence of char
|
| 1594 |
+
|
| 1595 |
+
char is included in the output, except if the end of the tile is
|
| 1596 |
+
encountered first.
|
| 1597 |
+
|
| 1598 |
+
Parameters
|
| 1599 |
+
----------
|
| 1600 |
+
char: bytes
|
| 1601 |
+
Thing to find
|
| 1602 |
+
blocks: None or int
|
| 1603 |
+
How much to read in each go. Defaults to file blocksize - which may
|
| 1604 |
+
mean a new read on every call.
|
| 1605 |
+
"""
|
| 1606 |
+
out = []
|
| 1607 |
+
while True:
|
| 1608 |
+
start = self.tell()
|
| 1609 |
+
part = self.read(blocks or self.blocksize)
|
| 1610 |
+
if len(part) == 0:
|
| 1611 |
+
break
|
| 1612 |
+
found = part.find(char)
|
| 1613 |
+
if found > -1:
|
| 1614 |
+
out.append(part[: found + len(char)])
|
| 1615 |
+
self.seek(start + found + len(char))
|
| 1616 |
+
break
|
| 1617 |
+
out.append(part)
|
| 1618 |
+
return b"".join(out)
|
| 1619 |
+
|
| 1620 |
+
def readline(self):
|
| 1621 |
+
"""Read until first occurrence of newline character
|
| 1622 |
+
|
| 1623 |
+
Note that, because of character encoding, this is not necessarily a
|
| 1624 |
+
true line ending.
|
| 1625 |
+
"""
|
| 1626 |
+
return self.readuntil(b"\n")
|
| 1627 |
+
|
| 1628 |
+
def __next__(self):
|
| 1629 |
+
out = self.readline()
|
| 1630 |
+
if out:
|
| 1631 |
+
return out
|
| 1632 |
+
raise StopIteration
|
| 1633 |
+
|
| 1634 |
+
def __iter__(self):
|
| 1635 |
+
return self
|
| 1636 |
+
|
| 1637 |
+
def readlines(self):
|
| 1638 |
+
"""Return all data, split by the newline character"""
|
| 1639 |
+
data = self.read()
|
| 1640 |
+
lines = data.split(b"\n")
|
| 1641 |
+
out = [l + b"\n" for l in lines[:-1]]
|
| 1642 |
+
if data.endswith(b"\n"):
|
| 1643 |
+
return out
|
| 1644 |
+
else:
|
| 1645 |
+
return out + [lines[-1]]
|
| 1646 |
+
# return list(self) ???
|
| 1647 |
+
|
| 1648 |
+
def readinto1(self, b):
|
| 1649 |
+
return self.readinto(b)
|
| 1650 |
+
|
| 1651 |
+
def close(self):
|
| 1652 |
+
"""Close file
|
| 1653 |
+
|
| 1654 |
+
Finalizes writes, discards cache
|
| 1655 |
+
"""
|
| 1656 |
+
if getattr(self, "_unclosable", False):
|
| 1657 |
+
return
|
| 1658 |
+
if self.closed:
|
| 1659 |
+
return
|
| 1660 |
+
if self.mode == "rb":
|
| 1661 |
+
self.cache = None
|
| 1662 |
+
else:
|
| 1663 |
+
if not self.forced:
|
| 1664 |
+
self.flush(force=True)
|
| 1665 |
+
|
| 1666 |
+
if self.fs is not None:
|
| 1667 |
+
self.fs.invalidate_cache(self.path)
|
| 1668 |
+
self.fs.invalidate_cache(self.fs._parent(self.path))
|
| 1669 |
+
|
| 1670 |
+
self.closed = True
|
| 1671 |
+
|
| 1672 |
+
def readable(self):
|
| 1673 |
+
"""Whether opened for reading"""
|
| 1674 |
+
return self.mode == "rb" and not self.closed
|
| 1675 |
+
|
| 1676 |
+
def seekable(self):
|
| 1677 |
+
"""Whether is seekable (only in read mode)"""
|
| 1678 |
+
return self.readable()
|
| 1679 |
+
|
| 1680 |
+
def writable(self):
|
| 1681 |
+
"""Whether opened for writing"""
|
| 1682 |
+
return self.mode in {"wb", "ab"} and not self.closed
|
| 1683 |
+
|
| 1684 |
+
def __del__(self):
|
| 1685 |
+
if not self.closed:
|
| 1686 |
+
self.close()
|
| 1687 |
+
|
| 1688 |
+
def __str__(self):
|
| 1689 |
+
return "<File-like object %s, %s>" % (type(self.fs).__name__, self.path)
|
| 1690 |
+
|
| 1691 |
+
__repr__ = __str__
|
| 1692 |
+
|
| 1693 |
+
def __enter__(self):
|
| 1694 |
+
return self
|
| 1695 |
+
|
| 1696 |
+
def __exit__(self, *args):
|
| 1697 |
+
self.close()
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/transaction.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class Transaction(object):
|
| 2 |
+
"""Filesystem transaction write context
|
| 3 |
+
|
| 4 |
+
Gathers files for deferred commit or discard, so that several write
|
| 5 |
+
operations can be finalized semi-atomically. This works by having this
|
| 6 |
+
instance as the ``.transaction`` attribute of the given filesystem
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
def __init__(self, fs):
|
| 10 |
+
"""
|
| 11 |
+
Parameters
|
| 12 |
+
----------
|
| 13 |
+
fs: FileSystem instance
|
| 14 |
+
"""
|
| 15 |
+
self.fs = fs
|
| 16 |
+
self.files = []
|
| 17 |
+
|
| 18 |
+
def __enter__(self):
|
| 19 |
+
self.start()
|
| 20 |
+
|
| 21 |
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
| 22 |
+
"""End transaction and commit, if exit is not due to exception"""
|
| 23 |
+
# only commit if there was no exception
|
| 24 |
+
self.complete(commit=exc_type is None)
|
| 25 |
+
self.fs._intrans = False
|
| 26 |
+
self.fs._transaction = None
|
| 27 |
+
|
| 28 |
+
def start(self):
|
| 29 |
+
"""Start a transaction on this FileSystem"""
|
| 30 |
+
self.files = [] # clean up after previous failed completions
|
| 31 |
+
self.fs._intrans = True
|
| 32 |
+
|
| 33 |
+
def complete(self, commit=True):
|
| 34 |
+
"""Finish transaction: commit or discard all deferred files"""
|
| 35 |
+
for f in self.files:
|
| 36 |
+
if commit:
|
| 37 |
+
f.commit()
|
| 38 |
+
else:
|
| 39 |
+
f.discard()
|
| 40 |
+
self.files = []
|
| 41 |
+
self.fs._intrans = False
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class FileActor(object):
|
| 45 |
+
def __init__(self):
|
| 46 |
+
self.files = []
|
| 47 |
+
|
| 48 |
+
def commit(self):
|
| 49 |
+
for f in self.files:
|
| 50 |
+
f.commit()
|
| 51 |
+
self.files.clear()
|
| 52 |
+
|
| 53 |
+
def discard(self):
|
| 54 |
+
for f in self.files:
|
| 55 |
+
f.discard()
|
| 56 |
+
self.files.clear()
|
| 57 |
+
|
| 58 |
+
def append(self, f):
|
| 59 |
+
self.files.append(f)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class DaskTransaction(Transaction):
|
| 63 |
+
def __init__(self, fs):
|
| 64 |
+
"""
|
| 65 |
+
Parameters
|
| 66 |
+
----------
|
| 67 |
+
fs: FileSystem instance
|
| 68 |
+
"""
|
| 69 |
+
import distributed
|
| 70 |
+
|
| 71 |
+
super().__init__(fs)
|
| 72 |
+
client = distributed.default_client()
|
| 73 |
+
self.files = client.submit(FileActor, actor=True).result()
|
| 74 |
+
|
| 75 |
+
def complete(self, commit=True):
|
| 76 |
+
"""Finish transaction: commit or discard all deferred files"""
|
| 77 |
+
if commit:
|
| 78 |
+
self.files.commit().result()
|
| 79 |
+
else:
|
| 80 |
+
self.files.discard().result()
|
| 81 |
+
self.fs._intrans = False
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/fsspec/utils.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import math
|
| 3 |
+
import os
|
| 4 |
+
import pathlib
|
| 5 |
+
import re
|
| 6 |
+
import sys
|
| 7 |
+
from contextlib import contextmanager
|
| 8 |
+
from functools import partial
|
| 9 |
+
from hashlib import md5
|
| 10 |
+
from types import TracebackType
|
| 11 |
+
from typing import IO, AnyStr, Callable, Iterable, Iterator, List, Optional, Type
|
| 12 |
+
from urllib.parse import urlsplit
|
| 13 |
+
|
| 14 |
+
DEFAULT_BLOCK_SIZE = 5 * 2**20
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def infer_storage_options(urlpath, inherit_storage_options=None):
|
| 18 |
+
"""Infer storage options from URL path and merge it with existing storage
|
| 19 |
+
options.
|
| 20 |
+
|
| 21 |
+
Parameters
|
| 22 |
+
----------
|
| 23 |
+
urlpath: str or unicode
|
| 24 |
+
Either local absolute file path or URL (hdfs://namenode:8020/file.csv)
|
| 25 |
+
inherit_storage_options: dict (optional)
|
| 26 |
+
Its contents will get merged with the inferred information from the
|
| 27 |
+
given path
|
| 28 |
+
|
| 29 |
+
Returns
|
| 30 |
+
-------
|
| 31 |
+
Storage options dict.
|
| 32 |
+
|
| 33 |
+
Examples
|
| 34 |
+
--------
|
| 35 |
+
>>> infer_storage_options('/mnt/datasets/test.csv') # doctest: +SKIP
|
| 36 |
+
{"protocol": "file", "path", "/mnt/datasets/test.csv"}
|
| 37 |
+
>>> infer_storage_options(
|
| 38 |
+
... 'hdfs://username:pwd@node:123/mnt/datasets/test.csv?q=1',
|
| 39 |
+
... inherit_storage_options={'extra': 'value'},
|
| 40 |
+
... ) # doctest: +SKIP
|
| 41 |
+
{"protocol": "hdfs", "username": "username", "password": "pwd",
|
| 42 |
+
"host": "node", "port": 123, "path": "/mnt/datasets/test.csv",
|
| 43 |
+
"url_query": "q=1", "extra": "value"}
|
| 44 |
+
"""
|
| 45 |
+
# Handle Windows paths including disk name in this special case
|
| 46 |
+
if (
|
| 47 |
+
re.match(r"^[a-zA-Z]:[\\/]", urlpath)
|
| 48 |
+
or re.match(r"^[a-zA-Z0-9]+://", urlpath) is None
|
| 49 |
+
):
|
| 50 |
+
return {"protocol": "file", "path": urlpath}
|
| 51 |
+
|
| 52 |
+
parsed_path = urlsplit(urlpath)
|
| 53 |
+
protocol = parsed_path.scheme or "file"
|
| 54 |
+
if parsed_path.fragment:
|
| 55 |
+
path = "#".join([parsed_path.path, parsed_path.fragment])
|
| 56 |
+
else:
|
| 57 |
+
path = parsed_path.path
|
| 58 |
+
if protocol == "file":
|
| 59 |
+
# Special case parsing file protocol URL on Windows according to:
|
| 60 |
+
# https://msdn.microsoft.com/en-us/library/jj710207.aspx
|
| 61 |
+
windows_path = re.match(r"^/([a-zA-Z])[:|]([\\/].*)$", path)
|
| 62 |
+
if windows_path:
|
| 63 |
+
path = "%s:%s" % windows_path.groups()
|
| 64 |
+
|
| 65 |
+
if protocol in ["http", "https"]:
|
| 66 |
+
# for HTTP, we don't want to parse, as requests will anyway
|
| 67 |
+
return {"protocol": protocol, "path": urlpath}
|
| 68 |
+
|
| 69 |
+
options = {"protocol": protocol, "path": path}
|
| 70 |
+
|
| 71 |
+
if parsed_path.netloc:
|
| 72 |
+
# Parse `hostname` from netloc manually because `parsed_path.hostname`
|
| 73 |
+
# lowercases the hostname which is not always desirable (e.g. in S3):
|
| 74 |
+
# https://github.com/dask/dask/issues/1417
|
| 75 |
+
options["host"] = parsed_path.netloc.rsplit("@", 1)[-1].rsplit(":", 1)[0]
|
| 76 |
+
|
| 77 |
+
if protocol in ("s3", "s3a", "gcs", "gs"):
|
| 78 |
+
options["path"] = options["host"] + options["path"]
|
| 79 |
+
else:
|
| 80 |
+
options["host"] = options["host"]
|
| 81 |
+
if parsed_path.port:
|
| 82 |
+
options["port"] = parsed_path.port
|
| 83 |
+
if parsed_path.username:
|
| 84 |
+
options["username"] = parsed_path.username
|
| 85 |
+
if parsed_path.password:
|
| 86 |
+
options["password"] = parsed_path.password
|
| 87 |
+
|
| 88 |
+
if parsed_path.query:
|
| 89 |
+
options["url_query"] = parsed_path.query
|
| 90 |
+
if parsed_path.fragment:
|
| 91 |
+
options["url_fragment"] = parsed_path.fragment
|
| 92 |
+
|
| 93 |
+
if inherit_storage_options:
|
| 94 |
+
update_storage_options(options, inherit_storage_options)
|
| 95 |
+
|
| 96 |
+
return options
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def update_storage_options(options, inherited=None):
|
| 100 |
+
if not inherited:
|
| 101 |
+
inherited = {}
|
| 102 |
+
collisions = set(options) & set(inherited)
|
| 103 |
+
if collisions:
|
| 104 |
+
for collision in collisions:
|
| 105 |
+
if options.get(collision) != inherited.get(collision):
|
| 106 |
+
raise KeyError(
|
| 107 |
+
"Collision between inferred and specified storage "
|
| 108 |
+
"option:\n%s" % collision
|
| 109 |
+
)
|
| 110 |
+
options.update(inherited)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# Compression extensions registered via fsspec.compression.register_compression
|
| 114 |
+
compressions = {}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def infer_compression(filename):
|
| 118 |
+
"""Infer compression, if available, from filename.
|
| 119 |
+
|
| 120 |
+
Infer a named compression type, if registered and available, from filename
|
| 121 |
+
extension. This includes builtin (gz, bz2, zip) compressions, as well as
|
| 122 |
+
optional compressions. See fsspec.compression.register_compression.
|
| 123 |
+
"""
|
| 124 |
+
extension = os.path.splitext(filename)[-1].strip(".").lower()
|
| 125 |
+
if extension in compressions:
|
| 126 |
+
return compressions[extension]
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def build_name_function(max_int):
|
| 130 |
+
"""Returns a function that receives a single integer
|
| 131 |
+
and returns it as a string padded by enough zero characters
|
| 132 |
+
to align with maximum possible integer
|
| 133 |
+
|
| 134 |
+
>>> name_f = build_name_function(57)
|
| 135 |
+
|
| 136 |
+
>>> name_f(7)
|
| 137 |
+
'07'
|
| 138 |
+
>>> name_f(31)
|
| 139 |
+
'31'
|
| 140 |
+
>>> build_name_function(1000)(42)
|
| 141 |
+
'0042'
|
| 142 |
+
>>> build_name_function(999)(42)
|
| 143 |
+
'042'
|
| 144 |
+
>>> build_name_function(0)(0)
|
| 145 |
+
'0'
|
| 146 |
+
"""
|
| 147 |
+
# handle corner cases max_int is 0 or exact power of 10
|
| 148 |
+
max_int += 1e-8
|
| 149 |
+
|
| 150 |
+
pad_length = int(math.ceil(math.log10(max_int)))
|
| 151 |
+
|
| 152 |
+
def name_function(i):
|
| 153 |
+
return str(i).zfill(pad_length)
|
| 154 |
+
|
| 155 |
+
return name_function
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def seek_delimiter(file, delimiter, blocksize):
|
| 159 |
+
r"""Seek current file to file start, file end, or byte after delimiter seq.
|
| 160 |
+
|
| 161 |
+
Seeks file to next chunk delimiter, where chunks are defined on file start,
|
| 162 |
+
a delimiting sequence, and file end. Use file.tell() to see location afterwards.
|
| 163 |
+
Note that file start is a valid split, so must be at offset > 0 to seek for
|
| 164 |
+
delimiter.
|
| 165 |
+
|
| 166 |
+
Parameters
|
| 167 |
+
----------
|
| 168 |
+
file: a file
|
| 169 |
+
delimiter: bytes
|
| 170 |
+
a delimiter like ``b'\n'`` or message sentinel, matching file .read() type
|
| 171 |
+
blocksize: int
|
| 172 |
+
Number of bytes to read from the file at once.
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
Returns
|
| 176 |
+
-------
|
| 177 |
+
Returns True if a delimiter was found, False if at file start or end.
|
| 178 |
+
|
| 179 |
+
"""
|
| 180 |
+
|
| 181 |
+
if file.tell() == 0:
|
| 182 |
+
# beginning-of-file, return without seek
|
| 183 |
+
return False
|
| 184 |
+
|
| 185 |
+
# Interface is for binary IO, with delimiter as bytes, but initialize last
|
| 186 |
+
# with result of file.read to preserve compatibility with text IO.
|
| 187 |
+
last = None
|
| 188 |
+
while True:
|
| 189 |
+
current = file.read(blocksize)
|
| 190 |
+
if not current:
|
| 191 |
+
# end-of-file without delimiter
|
| 192 |
+
return False
|
| 193 |
+
full = last + current if last else current
|
| 194 |
+
try:
|
| 195 |
+
if delimiter in full:
|
| 196 |
+
i = full.index(delimiter)
|
| 197 |
+
file.seek(file.tell() - (len(full) - i) + len(delimiter))
|
| 198 |
+
return True
|
| 199 |
+
elif len(current) < blocksize:
|
| 200 |
+
# end-of-file without delimiter
|
| 201 |
+
return False
|
| 202 |
+
except (OSError, ValueError):
|
| 203 |
+
pass
|
| 204 |
+
last = full[-len(delimiter) :]
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def read_block(f, offset, length, delimiter=None, split_before=False):
|
| 208 |
+
"""Read a block of bytes from a file
|
| 209 |
+
|
| 210 |
+
Parameters
|
| 211 |
+
----------
|
| 212 |
+
f: File
|
| 213 |
+
Open file
|
| 214 |
+
offset: int
|
| 215 |
+
Byte offset to start read
|
| 216 |
+
length: int
|
| 217 |
+
Number of bytes to read, read through end of file if None
|
| 218 |
+
delimiter: bytes (optional)
|
| 219 |
+
Ensure reading starts and stops at delimiter bytestring
|
| 220 |
+
split_before: bool (optional)
|
| 221 |
+
Start/stop read *before* delimiter bytestring.
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
If using the ``delimiter=`` keyword argument we ensure that the read
|
| 225 |
+
starts and stops at delimiter boundaries that follow the locations
|
| 226 |
+
``offset`` and ``offset + length``. If ``offset`` is zero then we
|
| 227 |
+
start at zero, regardless of delimiter. The bytestring returned WILL
|
| 228 |
+
include the terminating delimiter string.
|
| 229 |
+
|
| 230 |
+
Examples
|
| 231 |
+
--------
|
| 232 |
+
|
| 233 |
+
>>> from io import BytesIO # doctest: +SKIP
|
| 234 |
+
>>> f = BytesIO(b'Alice, 100\\nBob, 200\\nCharlie, 300') # doctest: +SKIP
|
| 235 |
+
>>> read_block(f, 0, 13) # doctest: +SKIP
|
| 236 |
+
b'Alice, 100\\nBo'
|
| 237 |
+
|
| 238 |
+
>>> read_block(f, 0, 13, delimiter=b'\\n') # doctest: +SKIP
|
| 239 |
+
b'Alice, 100\\nBob, 200\\n'
|
| 240 |
+
|
| 241 |
+
>>> read_block(f, 10, 10, delimiter=b'\\n') # doctest: +SKIP
|
| 242 |
+
b'Bob, 200\\nCharlie, 300'
|
| 243 |
+
"""
|
| 244 |
+
if delimiter:
|
| 245 |
+
f.seek(offset)
|
| 246 |
+
found_start_delim = seek_delimiter(f, delimiter, 2**16)
|
| 247 |
+
if length is None:
|
| 248 |
+
return f.read()
|
| 249 |
+
start = f.tell()
|
| 250 |
+
length -= start - offset
|
| 251 |
+
|
| 252 |
+
f.seek(start + length)
|
| 253 |
+
found_end_delim = seek_delimiter(f, delimiter, 2**16)
|
| 254 |
+
end = f.tell()
|
| 255 |
+
|
| 256 |
+
# Adjust split location to before delimiter iff seek found the
|
| 257 |
+
# delimiter sequence, not start or end of file.
|
| 258 |
+
if found_start_delim and split_before:
|
| 259 |
+
start -= len(delimiter)
|
| 260 |
+
|
| 261 |
+
if found_end_delim and split_before:
|
| 262 |
+
end -= len(delimiter)
|
| 263 |
+
|
| 264 |
+
offset = start
|
| 265 |
+
length = end - start
|
| 266 |
+
|
| 267 |
+
f.seek(offset)
|
| 268 |
+
b = f.read(length)
|
| 269 |
+
return b
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def tokenize(*args, **kwargs):
|
| 273 |
+
"""Deterministic token
|
| 274 |
+
|
| 275 |
+
(modified from dask.base)
|
| 276 |
+
|
| 277 |
+
>>> tokenize([1, 2, '3'])
|
| 278 |
+
'9d71491b50023b06fc76928e6eddb952'
|
| 279 |
+
|
| 280 |
+
>>> tokenize('Hello') == tokenize('Hello')
|
| 281 |
+
True
|
| 282 |
+
"""
|
| 283 |
+
if kwargs:
|
| 284 |
+
args += (kwargs,)
|
| 285 |
+
try:
|
| 286 |
+
return md5(str(args).encode()).hexdigest()
|
| 287 |
+
except ValueError:
|
| 288 |
+
# FIPS systems: https://github.com/fsspec/filesystem_spec/issues/380
|
| 289 |
+
return md5(str(args).encode(), usedforsecurity=False).hexdigest()
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def stringify_path(filepath):
|
| 293 |
+
"""Attempt to convert a path-like object to a string.
|
| 294 |
+
|
| 295 |
+
Parameters
|
| 296 |
+
----------
|
| 297 |
+
filepath: object to be converted
|
| 298 |
+
|
| 299 |
+
Returns
|
| 300 |
+
-------
|
| 301 |
+
filepath_str: maybe a string version of the object
|
| 302 |
+
|
| 303 |
+
Notes
|
| 304 |
+
-----
|
| 305 |
+
Objects supporting the fspath protocol are coerced according to its
|
| 306 |
+
__fspath__ method.
|
| 307 |
+
|
| 308 |
+
For backwards compatibility with older Python version, pathlib.Path
|
| 309 |
+
objects are specially coerced.
|
| 310 |
+
|
| 311 |
+
Any other object is passed through unchanged, which includes bytes,
|
| 312 |
+
strings, buffers, or anything else that's not even path-like.
|
| 313 |
+
"""
|
| 314 |
+
if isinstance(filepath, str):
|
| 315 |
+
return filepath
|
| 316 |
+
elif hasattr(filepath, "__fspath__"):
|
| 317 |
+
return filepath.__fspath__()
|
| 318 |
+
elif isinstance(filepath, pathlib.Path):
|
| 319 |
+
return str(filepath)
|
| 320 |
+
elif hasattr(filepath, "path"):
|
| 321 |
+
return filepath.path
|
| 322 |
+
else:
|
| 323 |
+
return filepath
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def make_instance(cls, args, kwargs):
|
| 327 |
+
inst = cls(*args, **kwargs)
|
| 328 |
+
inst._determine_worker()
|
| 329 |
+
return inst
|
| 330 |
+
|
| 331 |
+
|
| 332 |
+
def common_prefix(paths):
|
| 333 |
+
"""For a list of paths, find the shortest prefix common to all"""
|
| 334 |
+
parts = [p.split("/") for p in paths]
|
| 335 |
+
lmax = min(len(p) for p in parts)
|
| 336 |
+
end = 0
|
| 337 |
+
for i in range(lmax):
|
| 338 |
+
end = all(p[i] == parts[0][i] for p in parts)
|
| 339 |
+
if not end:
|
| 340 |
+
break
|
| 341 |
+
i += end
|
| 342 |
+
return "/".join(parts[0][:i])
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def other_paths(paths, path2, is_dir=None, exists=False):
|
| 346 |
+
"""In bulk file operations, construct a new file tree from a list of files
|
| 347 |
+
|
| 348 |
+
Parameters
|
| 349 |
+
----------
|
| 350 |
+
paths: list of str
|
| 351 |
+
The input file tree
|
| 352 |
+
path2: str or list of str
|
| 353 |
+
Root to construct the new list in. If this is already a list of str, we just
|
| 354 |
+
assert it has the right number of elements.
|
| 355 |
+
is_dir: bool (optional)
|
| 356 |
+
For the special case where the input in one element, whether to regard the value
|
| 357 |
+
as the target path, or as a directory to put a file path within. If None, a
|
| 358 |
+
directory is inferred if the path ends in '/'
|
| 359 |
+
exists: bool (optional)
|
| 360 |
+
For a str destination, it is already exists (and is a dir), files should
|
| 361 |
+
end up inside.
|
| 362 |
+
|
| 363 |
+
Returns
|
| 364 |
+
-------
|
| 365 |
+
list of str
|
| 366 |
+
"""
|
| 367 |
+
if isinstance(path2, str):
|
| 368 |
+
is_dir = is_dir or path2.endswith("/")
|
| 369 |
+
path2 = path2.rstrip("/")
|
| 370 |
+
if len(paths) > 1:
|
| 371 |
+
cp = common_prefix(paths)
|
| 372 |
+
if exists:
|
| 373 |
+
cp = cp.rsplit("/", 1)[0]
|
| 374 |
+
path2 = [p.replace(cp, path2, 1) for p in paths]
|
| 375 |
+
else:
|
| 376 |
+
if is_dir:
|
| 377 |
+
path2 = [path2.rstrip("/") + "/" + paths[0].rsplit("/")[-1]]
|
| 378 |
+
else:
|
| 379 |
+
path2 = [path2]
|
| 380 |
+
else:
|
| 381 |
+
assert len(paths) == len(path2)
|
| 382 |
+
return path2
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def is_exception(obj):
|
| 386 |
+
return isinstance(obj, BaseException)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def isfilelike(f):
|
| 390 |
+
for attr in ["read", "close", "tell"]:
|
| 391 |
+
if not hasattr(f, attr):
|
| 392 |
+
return False
|
| 393 |
+
return True
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
def get_protocol(url):
|
| 397 |
+
parts = re.split(r"(\:\:|\://)", url, 1)
|
| 398 |
+
if len(parts) > 1:
|
| 399 |
+
return parts[0]
|
| 400 |
+
return "file"
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def can_be_local(path):
|
| 404 |
+
"""Can the given URL be used with open_local?"""
|
| 405 |
+
from fsspec import get_filesystem_class
|
| 406 |
+
|
| 407 |
+
try:
|
| 408 |
+
return getattr(get_filesystem_class(get_protocol(path)), "local_file", False)
|
| 409 |
+
except (ValueError, ImportError):
|
| 410 |
+
# not in registry or import failed
|
| 411 |
+
return False
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def get_package_version_without_import(name):
|
| 415 |
+
"""For given package name, try to find the version without importing it
|
| 416 |
+
|
| 417 |
+
Import and package.__version__ is still the backup here, so an import
|
| 418 |
+
*might* happen.
|
| 419 |
+
|
| 420 |
+
Returns either the version string, or None if the package
|
| 421 |
+
or the version was not readily found.
|
| 422 |
+
"""
|
| 423 |
+
if name in sys.modules:
|
| 424 |
+
mod = sys.modules[name]
|
| 425 |
+
if hasattr(mod, "__version__"):
|
| 426 |
+
return mod.__version__
|
| 427 |
+
if sys.version_info >= (3, 8):
|
| 428 |
+
try:
|
| 429 |
+
import importlib.metadata
|
| 430 |
+
|
| 431 |
+
return importlib.metadata.distribution(name).version
|
| 432 |
+
except: # noqa: E722
|
| 433 |
+
pass
|
| 434 |
+
else:
|
| 435 |
+
try:
|
| 436 |
+
import importlib_metadata
|
| 437 |
+
|
| 438 |
+
return importlib_metadata.distribution(name).version
|
| 439 |
+
except: # noqa: E722
|
| 440 |
+
pass
|
| 441 |
+
try:
|
| 442 |
+
import importlib
|
| 443 |
+
|
| 444 |
+
mod = importlib.import_module(name)
|
| 445 |
+
return mod.__version__
|
| 446 |
+
except (ImportError, AttributeError):
|
| 447 |
+
return None
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def setup_logging(logger=None, logger_name=None, level="DEBUG", clear=True):
|
| 451 |
+
if logger is None and logger_name is None:
|
| 452 |
+
raise ValueError("Provide either logger object or logger name")
|
| 453 |
+
logger = logger or logging.getLogger(logger_name)
|
| 454 |
+
handle = logging.StreamHandler()
|
| 455 |
+
formatter = logging.Formatter(
|
| 456 |
+
"%(asctime)s - %(name)s - %(levelname)s - %(funcName)s -- %(message)s"
|
| 457 |
+
)
|
| 458 |
+
handle.setFormatter(formatter)
|
| 459 |
+
if clear:
|
| 460 |
+
logger.handlers.clear()
|
| 461 |
+
logger.addHandler(handle)
|
| 462 |
+
logger.setLevel(level)
|
| 463 |
+
return logger
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def _unstrip_protocol(name, fs):
|
| 467 |
+
return fs.unstrip_protocol(name)
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
def mirror_from(origin_name, methods):
|
| 471 |
+
"""Mirror attributes and methods from the given
|
| 472 |
+
origin_name attribute of the instance to the
|
| 473 |
+
decorated class"""
|
| 474 |
+
|
| 475 |
+
def origin_getter(method, self):
|
| 476 |
+
origin = getattr(self, origin_name)
|
| 477 |
+
return getattr(origin, method)
|
| 478 |
+
|
| 479 |
+
def wrapper(cls):
|
| 480 |
+
for method in methods:
|
| 481 |
+
wrapped_method = partial(origin_getter, method)
|
| 482 |
+
setattr(cls, method, property(wrapped_method))
|
| 483 |
+
return cls
|
| 484 |
+
|
| 485 |
+
return wrapper
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
@contextmanager
|
| 489 |
+
def nullcontext(obj):
|
| 490 |
+
yield obj
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def merge_offset_ranges(paths, starts, ends, max_gap=0, max_block=None, sort=True):
|
| 494 |
+
"""Merge adjacent byte-offset ranges when the inter-range
|
| 495 |
+
gap is <= `max_gap`, and when the merged byte range does not
|
| 496 |
+
exceed `max_block` (if specified). By default, this function
|
| 497 |
+
will re-order the input paths and byte ranges to ensure sorted
|
| 498 |
+
order. If the user can guarantee that the inputs are already
|
| 499 |
+
sorted, passing `sort=False` will skip the re-ordering.
|
| 500 |
+
"""
|
| 501 |
+
|
| 502 |
+
# Check input
|
| 503 |
+
if not isinstance(paths, list):
|
| 504 |
+
raise TypeError
|
| 505 |
+
if not isinstance(starts, list):
|
| 506 |
+
starts = [starts] * len(paths)
|
| 507 |
+
if not isinstance(ends, list):
|
| 508 |
+
ends = [starts] * len(paths)
|
| 509 |
+
if len(starts) != len(paths) or len(ends) != len(paths):
|
| 510 |
+
raise ValueError
|
| 511 |
+
|
| 512 |
+
# Early Return
|
| 513 |
+
if len(starts) <= 1:
|
| 514 |
+
return paths, starts, ends
|
| 515 |
+
|
| 516 |
+
# Sort by paths and then ranges if `sort=True`
|
| 517 |
+
if sort:
|
| 518 |
+
paths, starts, ends = [list(v) for v in zip(*sorted(zip(paths, starts, ends)))]
|
| 519 |
+
|
| 520 |
+
if paths:
|
| 521 |
+
# Loop through the coupled `paths`, `starts`, and
|
| 522 |
+
# `ends`, and merge adjacent blocks when appropriate
|
| 523 |
+
new_paths = paths[:1]
|
| 524 |
+
new_starts = starts[:1]
|
| 525 |
+
new_ends = ends[:1]
|
| 526 |
+
for i in range(1, len(paths)):
|
| 527 |
+
if (
|
| 528 |
+
paths[i] != paths[i - 1]
|
| 529 |
+
or ((starts[i] - new_ends[-1]) > max_gap)
|
| 530 |
+
or ((max_block is not None and (ends[i] - new_starts[-1]) > max_block))
|
| 531 |
+
):
|
| 532 |
+
# Cannot merge with previous block.
|
| 533 |
+
# Add new `paths`, `starts`, and `ends` elements
|
| 534 |
+
new_paths.append(paths[i])
|
| 535 |
+
new_starts.append(starts[i])
|
| 536 |
+
new_ends.append(ends[i])
|
| 537 |
+
else:
|
| 538 |
+
# Merge with previous block by updating the
|
| 539 |
+
# last element of `ends`
|
| 540 |
+
new_ends[-1] = ends[i]
|
| 541 |
+
return new_paths, new_starts, new_ends
|
| 542 |
+
|
| 543 |
+
# `paths` is empty. Just return input lists
|
| 544 |
+
return paths, starts, ends
|
| 545 |
+
|
| 546 |
+
|
| 547 |
+
class IOWrapper(IO):
|
| 548 |
+
"""Wrapper for a file-like object that can be used in situations where we might
|
| 549 |
+
want to, e.g., monkey-patch the close method but can't.
|
| 550 |
+
(cf https://github.com/fsspec/filesystem_spec/issues/725)
|
| 551 |
+
"""
|
| 552 |
+
|
| 553 |
+
def __init__(self, fp: IO, closer: Callable[[], None]):
|
| 554 |
+
self.fp = fp
|
| 555 |
+
self.closer = closer
|
| 556 |
+
|
| 557 |
+
def close(self) -> None:
|
| 558 |
+
self.fp.close()
|
| 559 |
+
|
| 560 |
+
def fileno(self) -> int:
|
| 561 |
+
return self.fp.fileno()
|
| 562 |
+
|
| 563 |
+
def flush(self) -> None:
|
| 564 |
+
self.fp.flush()
|
| 565 |
+
|
| 566 |
+
def isatty(self) -> bool:
|
| 567 |
+
return self.fp.isatty()
|
| 568 |
+
|
| 569 |
+
def read(self, n: int = ...) -> AnyStr:
|
| 570 |
+
return self.fp.read(n)
|
| 571 |
+
|
| 572 |
+
def readable(self) -> bool:
|
| 573 |
+
return self.fp.readable()
|
| 574 |
+
|
| 575 |
+
def readline(self, limit: int = ...) -> AnyStr:
|
| 576 |
+
return self.fp.readline(limit)
|
| 577 |
+
|
| 578 |
+
def readlines(self, hint: int = ...) -> List[AnyStr]:
|
| 579 |
+
return self.fp.readlines(hint)
|
| 580 |
+
|
| 581 |
+
def seek(self, offset: int, whence: int = ...) -> int:
|
| 582 |
+
return self.fp.seek(offset, whence)
|
| 583 |
+
|
| 584 |
+
def seekable(self) -> bool:
|
| 585 |
+
return self.fp.seekable()
|
| 586 |
+
|
| 587 |
+
def tell(self) -> int:
|
| 588 |
+
return self.fp.tell()
|
| 589 |
+
|
| 590 |
+
def truncate(self, size: Optional[int] = ...) -> int:
|
| 591 |
+
return self.fp.truncate(size)
|
| 592 |
+
|
| 593 |
+
def writable(self) -> bool:
|
| 594 |
+
return self.fp.writable()
|
| 595 |
+
|
| 596 |
+
def write(self, s: AnyStr) -> int:
|
| 597 |
+
return self.fp.write(s)
|
| 598 |
+
|
| 599 |
+
def writelines(self, lines: Iterable[AnyStr]) -> None:
|
| 600 |
+
self.fp.writelines(lines)
|
| 601 |
+
|
| 602 |
+
def __next__(self) -> AnyStr:
|
| 603 |
+
return next(self.fp)
|
| 604 |
+
|
| 605 |
+
def __iter__(self) -> Iterator[AnyStr]:
|
| 606 |
+
return iter(self.fp)
|
| 607 |
+
|
| 608 |
+
def __enter__(self) -> IO[AnyStr]:
|
| 609 |
+
return self.fp.__enter__()
|
| 610 |
+
|
| 611 |
+
def __exit__(
|
| 612 |
+
self,
|
| 613 |
+
t: Optional[Type[BaseException]],
|
| 614 |
+
value: Optional[BaseException],
|
| 615 |
+
traceback: Optional[TracebackType],
|
| 616 |
+
) -> Optional[bool]:
|
| 617 |
+
return self.fp.__exit__(t, value, traceback)
|
| 618 |
+
|
| 619 |
+
# forward anything else too
|
| 620 |
+
def __getattr__(self, name):
|
| 621 |
+
return getattr(self.fp, name)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/LICENSE.txt
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright (c) 2013-2019 Python Charmers Pty Ltd, Australia
|
| 2 |
+
|
| 3 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 4 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 5 |
+
in the Software without restriction, including without limitation the rights
|
| 6 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 7 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 8 |
+
furnished to do so, subject to the following conditions:
|
| 9 |
+
|
| 10 |
+
The above copyright notice and this permission notice shall be included in
|
| 11 |
+
all copies or substantial portions of the Software.
|
| 12 |
+
|
| 13 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 14 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 15 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 16 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 17 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 18 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
| 19 |
+
THE SOFTWARE.
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/METADATA
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.1
|
| 2 |
+
Name: future
|
| 3 |
+
Version: 0.18.2
|
| 4 |
+
Summary: Clean single-source support for Python 3 and 2
|
| 5 |
+
Home-page: https://python-future.org
|
| 6 |
+
Author: Ed Schofield
|
| 7 |
+
Author-email: ed@pythoncharmers.com
|
| 8 |
+
License: MIT
|
| 9 |
+
Keywords: future past python3 migration futurize backport six 2to3 modernize pasteurize 3to2
|
| 10 |
+
Classifier: Programming Language :: Python
|
| 11 |
+
Classifier: Programming Language :: Python :: 2
|
| 12 |
+
Classifier: Programming Language :: Python :: 2.6
|
| 13 |
+
Classifier: Programming Language :: Python :: 2.7
|
| 14 |
+
Classifier: Programming Language :: Python :: 3
|
| 15 |
+
Classifier: Programming Language :: Python :: 3.3
|
| 16 |
+
Classifier: Programming Language :: Python :: 3.4
|
| 17 |
+
Classifier: Programming Language :: Python :: 3.5
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.6
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.7
|
| 20 |
+
Classifier: License :: OSI Approved
|
| 21 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 22 |
+
Classifier: Development Status :: 4 - Beta
|
| 23 |
+
Classifier: Intended Audience :: Developers
|
| 24 |
+
Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.*
|
| 25 |
+
License-File: LICENSE.txt
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
future: Easy, safe support for Python 2/3 compatibility
|
| 29 |
+
=======================================================
|
| 30 |
+
|
| 31 |
+
``future`` is the missing compatibility layer between Python 2 and Python
|
| 32 |
+
3. It allows you to use a single, clean Python 3.x-compatible codebase to
|
| 33 |
+
support both Python 2 and Python 3 with minimal overhead.
|
| 34 |
+
|
| 35 |
+
It is designed to be used as follows::
|
| 36 |
+
|
| 37 |
+
from __future__ import (absolute_import, division,
|
| 38 |
+
print_function, unicode_literals)
|
| 39 |
+
from builtins import (
|
| 40 |
+
bytes, dict, int, list, object, range, str,
|
| 41 |
+
ascii, chr, hex, input, next, oct, open,
|
| 42 |
+
pow, round, super,
|
| 43 |
+
filter, map, zip)
|
| 44 |
+
|
| 45 |
+
followed by predominantly standard, idiomatic Python 3 code that then runs
|
| 46 |
+
similarly on Python 2.6/2.7 and Python 3.3+.
|
| 47 |
+
|
| 48 |
+
The imports have no effect on Python 3. On Python 2, they shadow the
|
| 49 |
+
corresponding builtins, which normally have different semantics on Python 3
|
| 50 |
+
versus 2, to provide their Python 3 semantics.
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
Standard library reorganization
|
| 54 |
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
| 55 |
+
|
| 56 |
+
``future`` supports the standard library reorganization (PEP 3108) through the
|
| 57 |
+
following Py3 interfaces:
|
| 58 |
+
|
| 59 |
+
>>> # Top-level packages with Py3 names provided on Py2:
|
| 60 |
+
>>> import html.parser
|
| 61 |
+
>>> import queue
|
| 62 |
+
>>> import tkinter.dialog
|
| 63 |
+
>>> import xmlrpc.client
|
| 64 |
+
>>> # etc.
|
| 65 |
+
|
| 66 |
+
>>> # Aliases provided for extensions to existing Py2 module names:
|
| 67 |
+
>>> from future.standard_library import install_aliases
|
| 68 |
+
>>> install_aliases()
|
| 69 |
+
|
| 70 |
+
>>> from collections import Counter, OrderedDict # backported to Py2.6
|
| 71 |
+
>>> from collections import UserDict, UserList, UserString
|
| 72 |
+
>>> import urllib.request
|
| 73 |
+
>>> from itertools import filterfalse, zip_longest
|
| 74 |
+
>>> from subprocess import getoutput, getstatusoutput
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
Automatic conversion
|
| 78 |
+
--------------------
|
| 79 |
+
|
| 80 |
+
An included script called `futurize
|
| 81 |
+
<http://python-future.org/automatic_conversion.html>`_ aids in converting
|
| 82 |
+
code (from either Python 2 or Python 3) to code compatible with both
|
| 83 |
+
platforms. It is similar to ``python-modernize`` but goes further in
|
| 84 |
+
providing Python 3 compatibility through the use of the backported types
|
| 85 |
+
and builtin functions in ``future``.
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
Documentation
|
| 89 |
+
-------------
|
| 90 |
+
|
| 91 |
+
See: http://python-future.org
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
Credits
|
| 95 |
+
-------
|
| 96 |
+
|
| 97 |
+
:Author: Ed Schofield, Jordan M. Adler, et al
|
| 98 |
+
:Sponsor: Python Charmers Pty Ltd, Australia, and Python Charmers Pte
|
| 99 |
+
Ltd, Singapore. http://pythoncharmers.com
|
| 100 |
+
:Others: See docs/credits.rst or http://python-future.org/credits.html
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
Licensing
|
| 104 |
+
---------
|
| 105 |
+
Copyright 2013-2019 Python Charmers Pty Ltd, Australia.
|
| 106 |
+
The software is distributed under an MIT licence. See LICENSE.txt.
|
| 107 |
+
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/RECORD
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../../bin/futurize,sha256=xKA_N8JYnXCyzVBfTUkz9pU3eXH5d8c16svN3USrvM4,233
|
| 2 |
+
../../../bin/pasteurize,sha256=GLIzOFPYAgHM9EHgTergrPuJuATuCzdvhzyepiNMEz8,235
|
| 3 |
+
future-0.18.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 4 |
+
future-0.18.2.dist-info/LICENSE.txt,sha256=kW5WE5LUhHG5wjQ39W4mUvMgyzsRnOqhYu30EBb3Rrk,1083
|
| 5 |
+
future-0.18.2.dist-info/METADATA,sha256=TpD2d2mu5U0tn4-xlVPIWgr_ND-bC8Bp0OSqjDXk2rw,3709
|
| 6 |
+
future-0.18.2.dist-info/RECORD,,
|
| 7 |
+
future-0.18.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 8 |
+
future-0.18.2.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92
|
| 9 |
+
future-0.18.2.dist-info/entry_points.txt,sha256=U9LtP60KSNXoj58mzV5TbotBF371gTWzrKrzJIH80Kw,88
|
| 10 |
+
future-0.18.2.dist-info/top_level.txt,sha256=DT0C3az2gb-uJaj-fs0h4WwHYlJVDp0EvLdud1y5Zyw,38
|
| 11 |
+
future/__init__.py,sha256=TsDq1XoGk6Jfach_rEhwAi07zR5OKYZ6hhUlG5Bj6Ag,2991
|
| 12 |
+
future/__pycache__/__init__.cpython-38.pyc,,
|
| 13 |
+
future/backports/__init__.py,sha256=5QXvQ_jc5Xx6p4dSaHnZXPZazBEunKDKhbUjxZ0XD1I,530
|
| 14 |
+
future/backports/__pycache__/__init__.cpython-38.pyc,,
|
| 15 |
+
future/backports/__pycache__/_markupbase.cpython-38.pyc,,
|
| 16 |
+
future/backports/__pycache__/datetime.cpython-38.pyc,,
|
| 17 |
+
future/backports/__pycache__/misc.cpython-38.pyc,,
|
| 18 |
+
future/backports/__pycache__/socket.cpython-38.pyc,,
|
| 19 |
+
future/backports/__pycache__/socketserver.cpython-38.pyc,,
|
| 20 |
+
future/backports/__pycache__/total_ordering.cpython-38.pyc,,
|
| 21 |
+
future/backports/_markupbase.py,sha256=MDPTCykLq4J7Aea3PvYotATEE0CG4R_SjlxfJaLXTJM,16215
|
| 22 |
+
future/backports/datetime.py,sha256=I214Vu0cRY8mi8J5aIcsAyQJnWmOKXeLV-QTWSn7VQU,75552
|
| 23 |
+
future/backports/email/__init__.py,sha256=eH3AJr3FkuBy_D6yS1V2K76Q2CQ93y2zmAMWmn8FbHI,2269
|
| 24 |
+
future/backports/email/__pycache__/__init__.cpython-38.pyc,,
|
| 25 |
+
future/backports/email/__pycache__/_encoded_words.cpython-38.pyc,,
|
| 26 |
+
future/backports/email/__pycache__/_header_value_parser.cpython-38.pyc,,
|
| 27 |
+
future/backports/email/__pycache__/_parseaddr.cpython-38.pyc,,
|
| 28 |
+
future/backports/email/__pycache__/_policybase.cpython-38.pyc,,
|
| 29 |
+
future/backports/email/__pycache__/base64mime.cpython-38.pyc,,
|
| 30 |
+
future/backports/email/__pycache__/charset.cpython-38.pyc,,
|
| 31 |
+
future/backports/email/__pycache__/encoders.cpython-38.pyc,,
|
| 32 |
+
future/backports/email/__pycache__/errors.cpython-38.pyc,,
|
| 33 |
+
future/backports/email/__pycache__/feedparser.cpython-38.pyc,,
|
| 34 |
+
future/backports/email/__pycache__/generator.cpython-38.pyc,,
|
| 35 |
+
future/backports/email/__pycache__/header.cpython-38.pyc,,
|
| 36 |
+
future/backports/email/__pycache__/headerregistry.cpython-38.pyc,,
|
| 37 |
+
future/backports/email/__pycache__/iterators.cpython-38.pyc,,
|
| 38 |
+
future/backports/email/__pycache__/message.cpython-38.pyc,,
|
| 39 |
+
future/backports/email/__pycache__/parser.cpython-38.pyc,,
|
| 40 |
+
future/backports/email/__pycache__/policy.cpython-38.pyc,,
|
| 41 |
+
future/backports/email/__pycache__/quoprimime.cpython-38.pyc,,
|
| 42 |
+
future/backports/email/__pycache__/utils.cpython-38.pyc,,
|
| 43 |
+
future/backports/email/_encoded_words.py,sha256=m1vTRfxAQdg4VyWO7PF-1ih1mmq97V-BPyHHkuEwSME,8443
|
| 44 |
+
future/backports/email/_header_value_parser.py,sha256=cj_1ce1voLn8H98r9cKqiSLgfFSxCv3_UL3sSvjqgjk,104692
|
| 45 |
+
future/backports/email/_parseaddr.py,sha256=KewEnos0YDM-SYX503z7E1MmVbG5VRaKjxjcl0Ipjbs,17389
|
| 46 |
+
future/backports/email/_policybase.py,sha256=2lJD9xouiz4uHvWGQ6j1nwlwWVQGwwzpy5JZoeQqhUc,14647
|
| 47 |
+
future/backports/email/base64mime.py,sha256=sey6iJA9pHIOdFgoV1p7QAwYVjt8CEkDhITt304-nyI,3729
|
| 48 |
+
future/backports/email/charset.py,sha256=CfE4iV2zAq6MQC0CHXHLnwTNW71zmhNITbzOcfxE4vY,17439
|
| 49 |
+
future/backports/email/encoders.py,sha256=Nn4Pcx1rOdRgoSIzB6T5RWHl5zxClbf32wgE6D0tUt8,2800
|
| 50 |
+
future/backports/email/errors.py,sha256=tRX8PP5g7mk2bAxL1jTCYrbfhD2gPZFNrh4_GJRM8OQ,3680
|
| 51 |
+
future/backports/email/feedparser.py,sha256=bvmhb4cdY-ipextPK2K2sDgMsNvTspmuQfYyCxc4zSc,22736
|
| 52 |
+
future/backports/email/generator.py,sha256=lpaLhZHneguvZ2QgRu7Figkjb7zmY28AGhj9iZTdI7s,19520
|
| 53 |
+
future/backports/email/header.py,sha256=uBHbNKO-yx5I9KBflernJpyy3fX4gImCB1QE7ICApLs,24448
|
| 54 |
+
future/backports/email/headerregistry.py,sha256=ZPbvLKXD0NMLSU4jXlVHfGyGcLMrFm-GQVURu_XHj88,20637
|
| 55 |
+
future/backports/email/iterators.py,sha256=kMRYFGy3SVVpo7HG7JJr2ZAlOoaX6CVPzKYwDSvLfV0,2348
|
| 56 |
+
future/backports/email/message.py,sha256=I6WW5cZDza7uwLOGJSvsDhGZC9K_Q570Lk2gt_vDUXM,35237
|
| 57 |
+
future/backports/email/mime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 58 |
+
future/backports/email/mime/__pycache__/__init__.cpython-38.pyc,,
|
| 59 |
+
future/backports/email/mime/__pycache__/application.cpython-38.pyc,,
|
| 60 |
+
future/backports/email/mime/__pycache__/audio.cpython-38.pyc,,
|
| 61 |
+
future/backports/email/mime/__pycache__/base.cpython-38.pyc,,
|
| 62 |
+
future/backports/email/mime/__pycache__/image.cpython-38.pyc,,
|
| 63 |
+
future/backports/email/mime/__pycache__/message.cpython-38.pyc,,
|
| 64 |
+
future/backports/email/mime/__pycache__/multipart.cpython-38.pyc,,
|
| 65 |
+
future/backports/email/mime/__pycache__/nonmultipart.cpython-38.pyc,,
|
| 66 |
+
future/backports/email/mime/__pycache__/text.cpython-38.pyc,,
|
| 67 |
+
future/backports/email/mime/application.py,sha256=m-5a4mSxu2E32XAImnp9x9eMVX5Vme2iNgn2dMMNyss,1401
|
| 68 |
+
future/backports/email/mime/audio.py,sha256=2ognalFRadcsUYQYMUZbjv5i1xJbFhQN643doMuI7M4,2815
|
| 69 |
+
future/backports/email/mime/base.py,sha256=wV3ClQyMsOqmkXSXbk_wd_zPoPTvBx8kAIzq3rdM4lE,875
|
| 70 |
+
future/backports/email/mime/image.py,sha256=DpQk1sB-IMmO43AF4uadsXyf_y5TdEzJLfyhqR48bIw,1907
|
| 71 |
+
future/backports/email/mime/message.py,sha256=pFsMhXW07aRjsLq1peO847PApWFAl28-Z2Z7BP1Dn74,1429
|
| 72 |
+
future/backports/email/mime/multipart.py,sha256=j4Lf_sJmuwTbfgdQ6R35_t1_ha2DynJBJDvpjwbNObE,1699
|
| 73 |
+
future/backports/email/mime/nonmultipart.py,sha256=Ciba1Z8d2yLDDpxgDJuk3Bb-TqcpE9HCd8KfbW5vgl4,832
|
| 74 |
+
future/backports/email/mime/text.py,sha256=zV98BjoR4S_nX8c47x43LnsnifeGhIfNGwSAh575bs0,1552
|
| 75 |
+
future/backports/email/parser.py,sha256=-115SC3DHZ6lLijWFTxuOnE-GiM2BOYaUSz-QpmvYSo,5312
|
| 76 |
+
future/backports/email/policy.py,sha256=gpcbhVRXuCohkK6MUqopTs1lv4E4-ZVUO6OVncoGEJE,8823
|
| 77 |
+
future/backports/email/quoprimime.py,sha256=w93W5XgdFpyGaDqDBJrnXF_v_npH5r20WuAxmrAzyQg,10923
|
| 78 |
+
future/backports/email/utils.py,sha256=vpfN0E8UjNbNw-2NFBQGCo4TNgrghMsqzpEYW5C_fBs,14270
|
| 79 |
+
future/backports/html/__init__.py,sha256=FKwqFtWMCoGNkhU97OPnR1fZSh6etAKfN1FU1KvXcV8,924
|
| 80 |
+
future/backports/html/__pycache__/__init__.cpython-38.pyc,,
|
| 81 |
+
future/backports/html/__pycache__/entities.cpython-38.pyc,,
|
| 82 |
+
future/backports/html/__pycache__/parser.cpython-38.pyc,,
|
| 83 |
+
future/backports/html/entities.py,sha256=kzoRnQyGk_3DgoucHLhL5QL1pglK9nvmxhPIGZFDTnc,75428
|
| 84 |
+
future/backports/html/parser.py,sha256=G2tUObvbHSotNt06JLY-BP1swaZNfDYFd_ENWDjPmRg,19770
|
| 85 |
+
future/backports/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 86 |
+
future/backports/http/__pycache__/__init__.cpython-38.pyc,,
|
| 87 |
+
future/backports/http/__pycache__/client.cpython-38.pyc,,
|
| 88 |
+
future/backports/http/__pycache__/cookiejar.cpython-38.pyc,,
|
| 89 |
+
future/backports/http/__pycache__/cookies.cpython-38.pyc,,
|
| 90 |
+
future/backports/http/__pycache__/server.cpython-38.pyc,,
|
| 91 |
+
future/backports/http/client.py,sha256=76EbhEZOtvdHFcU-jrjivoff13oQ9IMbdkZEdf5kQzQ,47602
|
| 92 |
+
future/backports/http/cookiejar.py,sha256=_Vy4BPT-h0ZT0R_utGQAFXzuOAdmU9KedGFffyX9wN4,76559
|
| 93 |
+
future/backports/http/cookies.py,sha256=DsyDUGDEbCXAA9Jq6suswSc76uSZqUu39adDDNj8XGw,21581
|
| 94 |
+
future/backports/http/server.py,sha256=1CaMxgzHf9lYhmTJyE7topgjRIlIn9cnjgw8YEvwJV4,45523
|
| 95 |
+
future/backports/misc.py,sha256=AkbED6BdHKnYCmIAontT4zHKTqdPPfJfn35HIs6LDrg,32682
|
| 96 |
+
future/backports/socket.py,sha256=DH1V6IjKPpJ0tln8bYvxvQ7qnvZG-UoQtMA5yVleHiU,15663
|
| 97 |
+
future/backports/socketserver.py,sha256=Twvyk5FqVnOeiNcbVsyMDPTF1mNlkKfyofG7tKxTdD8,24286
|
| 98 |
+
future/backports/test/__init__.py,sha256=9dXxIZnkI095YfHC-XIaVF6d31GjeY1Ag8TEzcFgepM,264
|
| 99 |
+
future/backports/test/__pycache__/__init__.cpython-38.pyc,,
|
| 100 |
+
future/backports/test/__pycache__/pystone.cpython-38.pyc,,
|
| 101 |
+
future/backports/test/__pycache__/ssl_servers.cpython-38.pyc,,
|
| 102 |
+
future/backports/test/__pycache__/support.cpython-38.pyc,,
|
| 103 |
+
future/backports/test/badcert.pem,sha256=JioQeRZkHH8hGsWJjAF3U1zQvcWqhyzG6IOEJpTY9SE,1928
|
| 104 |
+
future/backports/test/badkey.pem,sha256=gaBK9px_gG7DmrLKxfD6f6i-toAmARBTVfs-YGFRQF0,2162
|
| 105 |
+
future/backports/test/dh512.pem,sha256=dUTsjtLbK-femrorUrTGF8qvLjhTiT_n4Uo5V6u__Gs,402
|
| 106 |
+
future/backports/test/https_svn_python_org_root.pem,sha256=wOB3Onnc62Iu9kEFd8GcHhd_suucYjpJNA3jyfHeJWA,2569
|
| 107 |
+
future/backports/test/keycert.passwd.pem,sha256=ZBfnVLpbBtAOf_2gCdiQ-yrBHmRsNzSf8VC3UpQZIjg,1830
|
| 108 |
+
future/backports/test/keycert.pem,sha256=xPXi5idPcQVbrhgxBqF2TNGm6sSZ2aLVVEt6DWzplL8,1783
|
| 109 |
+
future/backports/test/keycert2.pem,sha256=DB46FEAYv8BWwQJ-5RzC696FxPN7CON-Qsi-R4poJgc,1795
|
| 110 |
+
future/backports/test/nokia.pem,sha256=s00x0uPDSaa5DHJ_CwzlVhg3OVdJ47f4zgqQdd0SAfQ,1923
|
| 111 |
+
future/backports/test/nullbytecert.pem,sha256=NFRYWhmP_qT3jGfVjR6-iaC-EQdhIFjiXtTLN5ZPKnE,5435
|
| 112 |
+
future/backports/test/nullcert.pem,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 113 |
+
future/backports/test/pystone.py,sha256=fvyoJ_tVovTNaxbJmdJMwr9F6SngY-U4ibULnd_wUqA,7427
|
| 114 |
+
future/backports/test/sha256.pem,sha256=3wB-GQqEc7jq-PYwYAQaPbtTvvr7stk_DVmZxFgehfA,8344
|
| 115 |
+
future/backports/test/ssl_cert.pem,sha256=M607jJNeIeHG9BlTf_jaQkPJI4nOxSJPn-zmEAaW43M,867
|
| 116 |
+
future/backports/test/ssl_key.passwd.pem,sha256=I_WH4sBw9Vs9Z-BvmuXY0aw8tx8avv6rm5UL4S_pP00,963
|
| 117 |
+
future/backports/test/ssl_key.pem,sha256=VKGU-R3UYaZpVTXl7chWl4vEYEDeob69SfvRTQ8aq_4,916
|
| 118 |
+
future/backports/test/ssl_servers.py,sha256=-pd7HMZljuZfFRAbCAiAP_2G04orITJFj-S9ddr6o84,7209
|
| 119 |
+
future/backports/test/support.py,sha256=zJrb-pz-Wu2dZwnNodg1v3w96zVq7ORuN-hOGOHbdA8,70881
|
| 120 |
+
future/backports/total_ordering.py,sha256=O3M57_IisQ-zW5hW20uxkfk4fTGsr0EF2tAKx3BksQo,1929
|
| 121 |
+
future/backports/urllib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 122 |
+
future/backports/urllib/__pycache__/__init__.cpython-38.pyc,,
|
| 123 |
+
future/backports/urllib/__pycache__/error.cpython-38.pyc,,
|
| 124 |
+
future/backports/urllib/__pycache__/parse.cpython-38.pyc,,
|
| 125 |
+
future/backports/urllib/__pycache__/request.cpython-38.pyc,,
|
| 126 |
+
future/backports/urllib/__pycache__/response.cpython-38.pyc,,
|
| 127 |
+
future/backports/urllib/__pycache__/robotparser.cpython-38.pyc,,
|
| 128 |
+
future/backports/urllib/error.py,sha256=ktikuK9ag4lS4f8Z0k5p1F11qF40N2AiOtjbXiF97ew,2715
|
| 129 |
+
future/backports/urllib/parse.py,sha256=67avrYqV1UK7i_22goRUrvJ8SffzjRdTja9wzq_ynXY,35792
|
| 130 |
+
future/backports/urllib/request.py,sha256=aR9ZMzfhV1C2Qk3wFsGvkwxqtdPTdsJVGRt5DUCwgJ8,96276
|
| 131 |
+
future/backports/urllib/response.py,sha256=ooQyswwbb-9N6IVi1Kwjss1aR-Kvm8ZNezoyVEonp8c,3180
|
| 132 |
+
future/backports/urllib/robotparser.py,sha256=pnAGTbKhdbCq_9yMZp7m8hj5q_NJpyQX6oQIZuYcnkw,6865
|
| 133 |
+
future/backports/xmlrpc/__init__.py,sha256=h61ciVTdVvu8oEUXv4dHf_Tc5XUXDH3RKB1-8fQhSsg,38
|
| 134 |
+
future/backports/xmlrpc/__pycache__/__init__.cpython-38.pyc,,
|
| 135 |
+
future/backports/xmlrpc/__pycache__/client.cpython-38.pyc,,
|
| 136 |
+
future/backports/xmlrpc/__pycache__/server.cpython-38.pyc,,
|
| 137 |
+
future/backports/xmlrpc/client.py,sha256=6a6Pvx_RVC9gIHDkFOVdREeGaZckOOiWd7T6GyzU3qU,48133
|
| 138 |
+
future/backports/xmlrpc/server.py,sha256=W_RW5hgYbNV2LGbnvngzm7akacRdK-XFY-Cy2HL-qsY,37285
|
| 139 |
+
future/builtins/__init__.py,sha256=jSdOucWfCsfkfTR8Jd4-Ls-YQpJ0AnzUomBxgwuoxNs,1687
|
| 140 |
+
future/builtins/__pycache__/__init__.cpython-38.pyc,,
|
| 141 |
+
future/builtins/__pycache__/disabled.cpython-38.pyc,,
|
| 142 |
+
future/builtins/__pycache__/iterators.cpython-38.pyc,,
|
| 143 |
+
future/builtins/__pycache__/misc.cpython-38.pyc,,
|
| 144 |
+
future/builtins/__pycache__/new_min_max.cpython-38.pyc,,
|
| 145 |
+
future/builtins/__pycache__/newnext.cpython-38.pyc,,
|
| 146 |
+
future/builtins/__pycache__/newround.cpython-38.pyc,,
|
| 147 |
+
future/builtins/__pycache__/newsuper.cpython-38.pyc,,
|
| 148 |
+
future/builtins/disabled.py,sha256=Ysq74bsmwntpq7dzkwTAD7IHKrkXy66vJlPshVwgVBI,2109
|
| 149 |
+
future/builtins/iterators.py,sha256=l1Zawm2x82oqOuGGtCZRE76Ej98sMlHQwu9fZLK5RrA,1396
|
| 150 |
+
future/builtins/misc.py,sha256=hctlKKWUyN0Eoodxg4ySQHEqARTukOLR4L5K5c6PW9k,4550
|
| 151 |
+
future/builtins/new_min_max.py,sha256=7qQ4iiG4GDgRzjPzzzmg9pdby35Mtt6xNOOsyqHnIGY,1757
|
| 152 |
+
future/builtins/newnext.py,sha256=oxXB8baXqJv29YG40aCS9UXk9zObyoOjya8BJ7NdBJM,2009
|
| 153 |
+
future/builtins/newround.py,sha256=l2EXPAFU3fAsZigJxUH6x66B7jhNaB076-L5FR617R8,3181
|
| 154 |
+
future/builtins/newsuper.py,sha256=LmiUQ_f6NXDIz6v6sDPkoTWl-2Zccy7PpZfQKYtscac,4146
|
| 155 |
+
future/moves/__init__.py,sha256=MsAW69Xp_fqUo4xODufcKM6AZf-ozHaz44WPZdsDFJA,220
|
| 156 |
+
future/moves/__pycache__/__init__.cpython-38.pyc,,
|
| 157 |
+
future/moves/__pycache__/_dummy_thread.cpython-38.pyc,,
|
| 158 |
+
future/moves/__pycache__/_markupbase.cpython-38.pyc,,
|
| 159 |
+
future/moves/__pycache__/_thread.cpython-38.pyc,,
|
| 160 |
+
future/moves/__pycache__/builtins.cpython-38.pyc,,
|
| 161 |
+
future/moves/__pycache__/collections.cpython-38.pyc,,
|
| 162 |
+
future/moves/__pycache__/configparser.cpython-38.pyc,,
|
| 163 |
+
future/moves/__pycache__/copyreg.cpython-38.pyc,,
|
| 164 |
+
future/moves/__pycache__/itertools.cpython-38.pyc,,
|
| 165 |
+
future/moves/__pycache__/pickle.cpython-38.pyc,,
|
| 166 |
+
future/moves/__pycache__/queue.cpython-38.pyc,,
|
| 167 |
+
future/moves/__pycache__/reprlib.cpython-38.pyc,,
|
| 168 |
+
future/moves/__pycache__/socketserver.cpython-38.pyc,,
|
| 169 |
+
future/moves/__pycache__/subprocess.cpython-38.pyc,,
|
| 170 |
+
future/moves/__pycache__/sys.cpython-38.pyc,,
|
| 171 |
+
future/moves/__pycache__/winreg.cpython-38.pyc,,
|
| 172 |
+
future/moves/_dummy_thread.py,sha256=c8ZRUd8ffvyvGKGGgve5NKc8VdtAWquu8-4FnO2EdvA,175
|
| 173 |
+
future/moves/_markupbase.py,sha256=W9wh_Gu3jDAMIhVBV1ZnCkJwYLHRk_v_su_HLALBkZQ,171
|
| 174 |
+
future/moves/_thread.py,sha256=rwY7L4BZMFPlrp_i6T2Un4_iKYwnrXJ-yV6FJZN8YDo,163
|
| 175 |
+
future/moves/builtins.py,sha256=4sjjKiylecJeL9da_RaBZjdymX2jtMs84oA9lCqb4Ug,281
|
| 176 |
+
future/moves/collections.py,sha256=OKQ-TfUgms_2bnZRn4hrclLDoiN2i-HSWcjs3BC2iY8,417
|
| 177 |
+
future/moves/configparser.py,sha256=TNy226uCbljjU-DjAVo7j7Effbj5zxXvDh0SdXehbzk,146
|
| 178 |
+
future/moves/copyreg.py,sha256=Y3UjLXIMSOxZggXtvZucE9yv4tkKZtVan45z8eix4sU,438
|
| 179 |
+
future/moves/dbm/__init__.py,sha256=_VkvQHC2UcIgZFPRroiX_P0Fs7HNqS_69flR0-oq2B8,488
|
| 180 |
+
future/moves/dbm/__pycache__/__init__.cpython-38.pyc,,
|
| 181 |
+
future/moves/dbm/__pycache__/dumb.cpython-38.pyc,,
|
| 182 |
+
future/moves/dbm/__pycache__/gnu.cpython-38.pyc,,
|
| 183 |
+
future/moves/dbm/__pycache__/ndbm.cpython-38.pyc,,
|
| 184 |
+
future/moves/dbm/dumb.py,sha256=HKdjjtO3EyP9EKi1Hgxh_eUU6yCQ0fBX9NN3n-zb8JE,166
|
| 185 |
+
future/moves/dbm/gnu.py,sha256=XoCSEpZ2QaOgo2h1m80GW7NUgj_b93BKtbcuwgtnaKo,162
|
| 186 |
+
future/moves/dbm/ndbm.py,sha256=OFnreyo_1YHDBl5YUm9gCzKlN1MHgWbfSQAZVls2jaM,162
|
| 187 |
+
future/moves/html/__init__.py,sha256=BSUFSHxXf2kGvHozlnrB1nn6bPE6p4PpN3DwA_Z5geo,1016
|
| 188 |
+
future/moves/html/__pycache__/__init__.cpython-38.pyc,,
|
| 189 |
+
future/moves/html/__pycache__/entities.cpython-38.pyc,,
|
| 190 |
+
future/moves/html/__pycache__/parser.cpython-38.pyc,,
|
| 191 |
+
future/moves/html/entities.py,sha256=lVvchdjK_RzRj759eg4RMvGWHfgBbj0tKGOoZ8dbRyY,177
|
| 192 |
+
future/moves/html/parser.py,sha256=V2XpHLKLCxQum3N9xlO3IUccAD7BIykZMqdEcWET3vY,167
|
| 193 |
+
future/moves/http/__init__.py,sha256=Mx1v_Tcks4udHCtDM8q2xnYUiQ01gD7EpPyeQwsP3-Q,71
|
| 194 |
+
future/moves/http/__pycache__/__init__.cpython-38.pyc,,
|
| 195 |
+
future/moves/http/__pycache__/client.cpython-38.pyc,,
|
| 196 |
+
future/moves/http/__pycache__/cookiejar.cpython-38.pyc,,
|
| 197 |
+
future/moves/http/__pycache__/cookies.cpython-38.pyc,,
|
| 198 |
+
future/moves/http/__pycache__/server.cpython-38.pyc,,
|
| 199 |
+
future/moves/http/client.py,sha256=hqEBq7GDXZidd1AscKnSyjSoMcuj8rERqGTmD7VheDQ,165
|
| 200 |
+
future/moves/http/cookiejar.py,sha256=Frr9ZZCg-145ymy0VGpiPJhvBEpJtVqRBYPaKhgT1Z4,173
|
| 201 |
+
future/moves/http/cookies.py,sha256=PPrHa1_oDbu3D_BhJGc6PvMgY1KoxyYq1jqeJwEcMvE,233
|
| 202 |
+
future/moves/http/server.py,sha256=8YQlSCShjAsB5rr5foVvZgp3IzwYFvTmGZCHhBSDtaI,606
|
| 203 |
+
future/moves/itertools.py,sha256=PVxFHRlBQl9ElS0cuGFPcUtj53eHX7Z1DmggzGfgQ6c,158
|
| 204 |
+
future/moves/pickle.py,sha256=r8j9skzfE8ZCeHyh_OB-WucOkRTIHN7zpRM7l7V3qS4,229
|
| 205 |
+
future/moves/queue.py,sha256=uxvLCChF-zxWWgrY1a_wxt8rp2jILdwO4PrnkBW6VTE,160
|
| 206 |
+
future/moves/reprlib.py,sha256=Nt5sUgMQ3jeVIukqSHOvB0UIsl6Y5t-mmT_13mpZmiY,161
|
| 207 |
+
future/moves/socketserver.py,sha256=v8ZLurDxHOgsubYm1iefjlpnnJQcx2VuRUGt9FCJB9k,174
|
| 208 |
+
future/moves/subprocess.py,sha256=oqRSMfFZkxM4MXkt3oD5N6eBwmmJ6rQ9KPhvSQKT_hM,251
|
| 209 |
+
future/moves/sys.py,sha256=HOMRX4Loim75FMbWawd3oEwuGNJR-ClMREEFkVpBsRs,132
|
| 210 |
+
future/moves/test/__init__.py,sha256=yB9F-fDQpzu1v8cBoKgIrL2ScUNqjlkqEztYrGVCQ-0,110
|
| 211 |
+
future/moves/test/__pycache__/__init__.cpython-38.pyc,,
|
| 212 |
+
future/moves/test/__pycache__/support.cpython-38.pyc,,
|
| 213 |
+
future/moves/test/support.py,sha256=6zGgTTXcERyBJIQ04-X-sAe781tVgLVHp3HzmQPy52g,259
|
| 214 |
+
future/moves/tkinter/__init__.py,sha256=jV9vDx3wRl0bsoclU8oSe-5SqHQ3YpCbStmqtXnq1p4,620
|
| 215 |
+
future/moves/tkinter/__pycache__/__init__.cpython-38.pyc,,
|
| 216 |
+
future/moves/tkinter/__pycache__/colorchooser.cpython-38.pyc,,
|
| 217 |
+
future/moves/tkinter/__pycache__/commondialog.cpython-38.pyc,,
|
| 218 |
+
future/moves/tkinter/__pycache__/constants.cpython-38.pyc,,
|
| 219 |
+
future/moves/tkinter/__pycache__/dialog.cpython-38.pyc,,
|
| 220 |
+
future/moves/tkinter/__pycache__/dnd.cpython-38.pyc,,
|
| 221 |
+
future/moves/tkinter/__pycache__/filedialog.cpython-38.pyc,,
|
| 222 |
+
future/moves/tkinter/__pycache__/font.cpython-38.pyc,,
|
| 223 |
+
future/moves/tkinter/__pycache__/messagebox.cpython-38.pyc,,
|
| 224 |
+
future/moves/tkinter/__pycache__/scrolledtext.cpython-38.pyc,,
|
| 225 |
+
future/moves/tkinter/__pycache__/simpledialog.cpython-38.pyc,,
|
| 226 |
+
future/moves/tkinter/__pycache__/tix.cpython-38.pyc,,
|
| 227 |
+
future/moves/tkinter/__pycache__/ttk.cpython-38.pyc,,
|
| 228 |
+
future/moves/tkinter/colorchooser.py,sha256=kprlmpRtvDbW5Gq43H1mi2KmNJ2kuzLQOba0a5EwDkU,333
|
| 229 |
+
future/moves/tkinter/commondialog.py,sha256=mdUbq1IZqOGaSA7_8R367IukDCsMfzXiVHrTQQpp7Z0,333
|
| 230 |
+
future/moves/tkinter/constants.py,sha256=0qRUrZLRPdVxueABL9KTzzEWEsk6xM1rOjxK6OHxXtA,324
|
| 231 |
+
future/moves/tkinter/dialog.py,sha256=ksp-zvs-_A90P9RNHS8S27f1k8f48zG2Bel2jwZV5y0,311
|
| 232 |
+
future/moves/tkinter/dnd.py,sha256=C_Ah0Urnyf2XKE5u-oP6mWi16RzMSXgMA1uhBSAwKY8,306
|
| 233 |
+
future/moves/tkinter/filedialog.py,sha256=RSJFDGOP2AJ4T0ZscJ2hyF9ssOWp9t_S_DtnOmT-WZ8,323
|
| 234 |
+
future/moves/tkinter/font.py,sha256=TXarflhJRxqepaRNSDw6JFUVGz5P1T1C4_uF9VRqj3w,309
|
| 235 |
+
future/moves/tkinter/messagebox.py,sha256=WJt4t83kLmr_UnpCWFuLoyazZr3wAUOEl6ADn3osoEA,327
|
| 236 |
+
future/moves/tkinter/scrolledtext.py,sha256=DRzN8aBAlDBUo1B2KDHzdpRSzXBfH4rOOz0iuHXbQcg,329
|
| 237 |
+
future/moves/tkinter/simpledialog.py,sha256=6MhuVhZCJV4XfPpPSUWKlDLLGEi0Y2ZlGQ9TbsmJFL0,329
|
| 238 |
+
future/moves/tkinter/tix.py,sha256=aNeOfbWSGmcN69UmEGf4tJ-QIxLT6SU5ynzm1iWgepA,302
|
| 239 |
+
future/moves/tkinter/ttk.py,sha256=rRrJpDjcP2gjQNukECu4F026P-CkW-3Ca2tN6Oia-Fw,302
|
| 240 |
+
future/moves/urllib/__init__.py,sha256=yB9F-fDQpzu1v8cBoKgIrL2ScUNqjlkqEztYrGVCQ-0,110
|
| 241 |
+
future/moves/urllib/__pycache__/__init__.cpython-38.pyc,,
|
| 242 |
+
future/moves/urllib/__pycache__/error.cpython-38.pyc,,
|
| 243 |
+
future/moves/urllib/__pycache__/parse.cpython-38.pyc,,
|
| 244 |
+
future/moves/urllib/__pycache__/request.cpython-38.pyc,,
|
| 245 |
+
future/moves/urllib/__pycache__/response.cpython-38.pyc,,
|
| 246 |
+
future/moves/urllib/__pycache__/robotparser.cpython-38.pyc,,
|
| 247 |
+
future/moves/urllib/error.py,sha256=gfrKzv-6W5OjzNIfjvJaQkxABRLym2KwjfKFXSdDB60,479
|
| 248 |
+
future/moves/urllib/parse.py,sha256=xLLUMIIB5MreCdYzRZ5zIRWrhTRCoMO8RZEH4WPFQDY,1045
|
| 249 |
+
future/moves/urllib/request.py,sha256=ttIzq60PwjRyrLQUGdAtfYvs4fziVwvcLe2Kw-hvE0g,3496
|
| 250 |
+
future/moves/urllib/response.py,sha256=ZEZML0FpbB--GIeBFPvSzbtlVJ6EsR4tCI4qB7D8sFQ,342
|
| 251 |
+
future/moves/urllib/robotparser.py,sha256=j24p6dMNzUpGZtT3BQxwRoE-F88iWmBpKgu0tRV61FQ,179
|
| 252 |
+
future/moves/winreg.py,sha256=2zNAG59QI7vFlCj7kqDh0JrAYTpexOnI55PEAIjYhqo,163
|
| 253 |
+
future/moves/xmlrpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 254 |
+
future/moves/xmlrpc/__pycache__/__init__.cpython-38.pyc,,
|
| 255 |
+
future/moves/xmlrpc/__pycache__/client.cpython-38.pyc,,
|
| 256 |
+
future/moves/xmlrpc/__pycache__/server.cpython-38.pyc,,
|
| 257 |
+
future/moves/xmlrpc/client.py,sha256=2PfnL5IbKVwdKP7C8B1OUviEtuBObwoH4pAPfvHIvQc,143
|
| 258 |
+
future/moves/xmlrpc/server.py,sha256=ESDXdpUgTKyeFmCDSnJmBp8zONjJklsRJOvy4OtaALc,143
|
| 259 |
+
future/standard_library/__init__.py,sha256=7paz9IsD5qv_tvk5Rre3YrlA2_2aS1FJfI7UlrzAtWY,27743
|
| 260 |
+
future/standard_library/__pycache__/__init__.cpython-38.pyc,,
|
| 261 |
+
future/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 262 |
+
future/tests/__pycache__/__init__.cpython-38.pyc,,
|
| 263 |
+
future/tests/__pycache__/base.cpython-38.pyc,,
|
| 264 |
+
future/tests/base.py,sha256=7LTAKHJgUxOwmffD1kgcErVt2VouKcldPnq4iruqk_k,19956
|
| 265 |
+
future/types/__init__.py,sha256=5fBxWqf_OTQ8jZ7k2TS34rFH14togeR488F4zBHIQ-s,6831
|
| 266 |
+
future/types/__pycache__/__init__.cpython-38.pyc,,
|
| 267 |
+
future/types/__pycache__/newbytes.cpython-38.pyc,,
|
| 268 |
+
future/types/__pycache__/newdict.cpython-38.pyc,,
|
| 269 |
+
future/types/__pycache__/newint.cpython-38.pyc,,
|
| 270 |
+
future/types/__pycache__/newlist.cpython-38.pyc,,
|
| 271 |
+
future/types/__pycache__/newmemoryview.cpython-38.pyc,,
|
| 272 |
+
future/types/__pycache__/newobject.cpython-38.pyc,,
|
| 273 |
+
future/types/__pycache__/newopen.cpython-38.pyc,,
|
| 274 |
+
future/types/__pycache__/newrange.cpython-38.pyc,,
|
| 275 |
+
future/types/__pycache__/newstr.cpython-38.pyc,,
|
| 276 |
+
future/types/newbytes.py,sha256=D_kNDD9sbNJir2cUxxePiAuw2OW5irxVnu55uHmuK9E,16303
|
| 277 |
+
future/types/newdict.py,sha256=2N7P44cWmWtiDHvlK5ir15mW492gg6uP2n65d5bsDy4,3100
|
| 278 |
+
future/types/newint.py,sha256=hJiv9qUDrjl1xkfzNFNLzafsRMPoFcRFceoivUzVIek,13286
|
| 279 |
+
future/types/newlist.py,sha256=-H5-fXodd-UQgTFnZBJdwE68CrgIL_jViYdv4w7q2rU,2284
|
| 280 |
+
future/types/newmemoryview.py,sha256=LnARgiKqQ2zLwwDZ3owu1atoonPQkOneWMfxJCwB4_o,712
|
| 281 |
+
future/types/newobject.py,sha256=AX_n8GwlDR2IY-xIwZCvu0Olj_Ca2aS57nlTihnFr-I,3358
|
| 282 |
+
future/types/newopen.py,sha256=lcRNHWZ1UjEn_0_XKis1ZA5U6l-4c-CHlC0WX1sY4NI,810
|
| 283 |
+
future/types/newrange.py,sha256=7sgJaRaC4WIUtZ40K-c1d5QWruyaCWGgTVFadKo8qYA,5294
|
| 284 |
+
future/types/newstr.py,sha256=e0brkurI0IK--4ToQEO4Cz1FECZav4CyUGMKxlrcmK4,15758
|
| 285 |
+
future/utils/__init__.py,sha256=wsvXsKx-DXZichQ10Rdml-CWMqS79RNNynmdvfISpCU,21828
|
| 286 |
+
future/utils/__pycache__/__init__.cpython-38.pyc,,
|
| 287 |
+
future/utils/__pycache__/surrogateescape.cpython-38.pyc,,
|
| 288 |
+
future/utils/surrogateescape.py,sha256=7u4V4XlW83P5YSAJS2f92YUF8vsWthsiTnmAshOJL_M,6097
|
| 289 |
+
libfuturize/__init__.py,sha256=CZA_KgvTQOPAY1_MrlJeQ6eMh2Eei4_KIv4JuyAkpfw,31
|
| 290 |
+
libfuturize/__pycache__/__init__.cpython-38.pyc,,
|
| 291 |
+
libfuturize/__pycache__/fixer_util.cpython-38.pyc,,
|
| 292 |
+
libfuturize/__pycache__/main.cpython-38.pyc,,
|
| 293 |
+
libfuturize/fixer_util.py,sha256=Zhms5G97l40pyG1krQM2lCp-TxnocBdJkB2AbkAFnKY,17494
|
| 294 |
+
libfuturize/fixes/__init__.py,sha256=5KEpUnjVsFCCsr_-zrikvJbLf9zslEJnFTH_5pBc33I,5236
|
| 295 |
+
libfuturize/fixes/__pycache__/__init__.cpython-38.pyc,,
|
| 296 |
+
libfuturize/fixes/__pycache__/fix_UserDict.cpython-38.pyc,,
|
| 297 |
+
libfuturize/fixes/__pycache__/fix_absolute_import.cpython-38.pyc,,
|
| 298 |
+
libfuturize/fixes/__pycache__/fix_add__future__imports_except_unicode_literals.cpython-38.pyc,,
|
| 299 |
+
libfuturize/fixes/__pycache__/fix_basestring.cpython-38.pyc,,
|
| 300 |
+
libfuturize/fixes/__pycache__/fix_bytes.cpython-38.pyc,,
|
| 301 |
+
libfuturize/fixes/__pycache__/fix_cmp.cpython-38.pyc,,
|
| 302 |
+
libfuturize/fixes/__pycache__/fix_division.cpython-38.pyc,,
|
| 303 |
+
libfuturize/fixes/__pycache__/fix_division_safe.cpython-38.pyc,,
|
| 304 |
+
libfuturize/fixes/__pycache__/fix_execfile.cpython-38.pyc,,
|
| 305 |
+
libfuturize/fixes/__pycache__/fix_future_builtins.cpython-38.pyc,,
|
| 306 |
+
libfuturize/fixes/__pycache__/fix_future_standard_library.cpython-38.pyc,,
|
| 307 |
+
libfuturize/fixes/__pycache__/fix_future_standard_library_urllib.cpython-38.pyc,,
|
| 308 |
+
libfuturize/fixes/__pycache__/fix_input.cpython-38.pyc,,
|
| 309 |
+
libfuturize/fixes/__pycache__/fix_metaclass.cpython-38.pyc,,
|
| 310 |
+
libfuturize/fixes/__pycache__/fix_next_call.cpython-38.pyc,,
|
| 311 |
+
libfuturize/fixes/__pycache__/fix_object.cpython-38.pyc,,
|
| 312 |
+
libfuturize/fixes/__pycache__/fix_oldstr_wrap.cpython-38.pyc,,
|
| 313 |
+
libfuturize/fixes/__pycache__/fix_order___future__imports.cpython-38.pyc,,
|
| 314 |
+
libfuturize/fixes/__pycache__/fix_print.cpython-38.pyc,,
|
| 315 |
+
libfuturize/fixes/__pycache__/fix_print_with_import.cpython-38.pyc,,
|
| 316 |
+
libfuturize/fixes/__pycache__/fix_raise.cpython-38.pyc,,
|
| 317 |
+
libfuturize/fixes/__pycache__/fix_remove_old__future__imports.cpython-38.pyc,,
|
| 318 |
+
libfuturize/fixes/__pycache__/fix_unicode_keep_u.cpython-38.pyc,,
|
| 319 |
+
libfuturize/fixes/__pycache__/fix_unicode_literals_import.cpython-38.pyc,,
|
| 320 |
+
libfuturize/fixes/__pycache__/fix_xrange_with_import.cpython-38.pyc,,
|
| 321 |
+
libfuturize/fixes/fix_UserDict.py,sha256=jL4jXnGaUQTkG8RKfGXbU_HVTkB3MWZMQwUkqMAjB6I,3840
|
| 322 |
+
libfuturize/fixes/fix_absolute_import.py,sha256=vkrF2FyQR5lSz2WmdqywzkEJVTC0eq4gh_REWBKHh7w,3140
|
| 323 |
+
libfuturize/fixes/fix_add__future__imports_except_unicode_literals.py,sha256=Fr219VAzR8KWXc2_bfiqLl10EgxAWjL6cI3Mowt--VU,662
|
| 324 |
+
libfuturize/fixes/fix_basestring.py,sha256=bHkKuMzhr5FMXwjXlMOjsod4S3rQkVdbzhoWV4-tl3Y,394
|
| 325 |
+
libfuturize/fixes/fix_bytes.py,sha256=AhzOJes6EnPwgzboDjvURANbWKqciG6ZGaYW07PYQK8,685
|
| 326 |
+
libfuturize/fixes/fix_cmp.py,sha256=Blq_Z0IGkYiKS83QzZ5wUgpJyZfQiZoEsWJ1VPyXgFY,701
|
| 327 |
+
libfuturize/fixes/fix_division.py,sha256=gnrAi7stquiVUyi_De1H8q--43iQaSUX0CjnOmQ6O2w,228
|
| 328 |
+
libfuturize/fixes/fix_division_safe.py,sha256=Y_HUfQJAxRClXkcfqWP5SFCsRYZOsLUsNjLXlGOA3cQ,3292
|
| 329 |
+
libfuturize/fixes/fix_execfile.py,sha256=I5AcJ6vPZ7i70TChaq9inxqnZ4C04-yJyfAItGa8E3c,921
|
| 330 |
+
libfuturize/fixes/fix_future_builtins.py,sha256=QBCRpD9XA7tbtfP4wmOF2DXquB4lq-eupkQj-QAxp0s,2027
|
| 331 |
+
libfuturize/fixes/fix_future_standard_library.py,sha256=FVtflFt38efHe_SEX6k3m6IYAtKWjA4rAPZrlCv6yA0,733
|
| 332 |
+
libfuturize/fixes/fix_future_standard_library_urllib.py,sha256=Rf81XcAXA-vwNvrhskf5sLExbR--Wkr5fiUcMYGAKzs,1001
|
| 333 |
+
libfuturize/fixes/fix_input.py,sha256=bhaPNtMrZNbjWIDQCR7Iue5BxBj4rf0RJQ9_jiwvb-s,687
|
| 334 |
+
libfuturize/fixes/fix_metaclass.py,sha256=GLB76wbuyUVciDgW9bgNNOBEnLeS_AR-fKABcPBZk6M,9568
|
| 335 |
+
libfuturize/fixes/fix_next_call.py,sha256=01STG86Av9o5QcpQDJ6UbPhvxt9kKrkatiPeddXRgvA,3158
|
| 336 |
+
libfuturize/fixes/fix_object.py,sha256=qalFIjn0VTWXG5sGOOoCvO65omjX5_9d40SUpwUjBdw,407
|
| 337 |
+
libfuturize/fixes/fix_oldstr_wrap.py,sha256=UCR6Q2l-pVqJSrRTnQAWMlaqBoX7oX1VpG_w6Q0XcyY,1214
|
| 338 |
+
libfuturize/fixes/fix_order___future__imports.py,sha256=ACUCw5NEGWvj6XA9rNj8BYha3ktxLvkM5Ssh5cyV644,829
|
| 339 |
+
libfuturize/fixes/fix_print.py,sha256=92s1w2t9SynA3Y1_85-lexSBbgEWJM6lBrhCxVacfDc,3384
|
| 340 |
+
libfuturize/fixes/fix_print_with_import.py,sha256=hVWn70Q1DPMUiHMyEqgUx-6sM1AylLj78v9pMc4LFw8,735
|
| 341 |
+
libfuturize/fixes/fix_raise.py,sha256=mEXpM9sS6tenMmxayfqM-Kp9gUvaztTY61vFaqyMUuo,3884
|
| 342 |
+
libfuturize/fixes/fix_remove_old__future__imports.py,sha256=j4EC1KEVgXhuQAqhYHnAruUjW6uczPjV_fTCSOLMuAw,851
|
| 343 |
+
libfuturize/fixes/fix_unicode_keep_u.py,sha256=M8fcFxHeFnWVOKoQRpkMsnpd9qmUFubI2oFhO4ZPk7A,779
|
| 344 |
+
libfuturize/fixes/fix_unicode_literals_import.py,sha256=wq-hb-9Yx3Az4ol-ylXZJPEDZ81EaPZeIy5VvpA0CEY,367
|
| 345 |
+
libfuturize/fixes/fix_xrange_with_import.py,sha256=f074qStjMz3OtLjt1bKKZSxQnRbbb7HzEbqHt9wgqdw,479
|
| 346 |
+
libfuturize/main.py,sha256=feICmcv0dzWhutvwz0unnIVxusbSlQZFDaxObkHebs8,13733
|
| 347 |
+
libpasteurize/__init__.py,sha256=CZA_KgvTQOPAY1_MrlJeQ6eMh2Eei4_KIv4JuyAkpfw,31
|
| 348 |
+
libpasteurize/__pycache__/__init__.cpython-38.pyc,,
|
| 349 |
+
libpasteurize/__pycache__/main.cpython-38.pyc,,
|
| 350 |
+
libpasteurize/fixes/__init__.py,sha256=ccdv-2MGjQMbq8XuEZBndHmbzGRrZnabksjXZLUv044,3719
|
| 351 |
+
libpasteurize/fixes/__pycache__/__init__.cpython-38.pyc,,
|
| 352 |
+
libpasteurize/fixes/__pycache__/feature_base.cpython-38.pyc,,
|
| 353 |
+
libpasteurize/fixes/__pycache__/fix_add_all__future__imports.cpython-38.pyc,,
|
| 354 |
+
libpasteurize/fixes/__pycache__/fix_add_all_future_builtins.cpython-38.pyc,,
|
| 355 |
+
libpasteurize/fixes/__pycache__/fix_add_future_standard_library_import.cpython-38.pyc,,
|
| 356 |
+
libpasteurize/fixes/__pycache__/fix_annotations.cpython-38.pyc,,
|
| 357 |
+
libpasteurize/fixes/__pycache__/fix_division.cpython-38.pyc,,
|
| 358 |
+
libpasteurize/fixes/__pycache__/fix_features.cpython-38.pyc,,
|
| 359 |
+
libpasteurize/fixes/__pycache__/fix_fullargspec.cpython-38.pyc,,
|
| 360 |
+
libpasteurize/fixes/__pycache__/fix_future_builtins.cpython-38.pyc,,
|
| 361 |
+
libpasteurize/fixes/__pycache__/fix_getcwd.cpython-38.pyc,,
|
| 362 |
+
libpasteurize/fixes/__pycache__/fix_imports.cpython-38.pyc,,
|
| 363 |
+
libpasteurize/fixes/__pycache__/fix_imports2.cpython-38.pyc,,
|
| 364 |
+
libpasteurize/fixes/__pycache__/fix_kwargs.cpython-38.pyc,,
|
| 365 |
+
libpasteurize/fixes/__pycache__/fix_memoryview.cpython-38.pyc,,
|
| 366 |
+
libpasteurize/fixes/__pycache__/fix_metaclass.cpython-38.pyc,,
|
| 367 |
+
libpasteurize/fixes/__pycache__/fix_newstyle.cpython-38.pyc,,
|
| 368 |
+
libpasteurize/fixes/__pycache__/fix_next.cpython-38.pyc,,
|
| 369 |
+
libpasteurize/fixes/__pycache__/fix_printfunction.cpython-38.pyc,,
|
| 370 |
+
libpasteurize/fixes/__pycache__/fix_raise.cpython-38.pyc,,
|
| 371 |
+
libpasteurize/fixes/__pycache__/fix_raise_.cpython-38.pyc,,
|
| 372 |
+
libpasteurize/fixes/__pycache__/fix_throw.cpython-38.pyc,,
|
| 373 |
+
libpasteurize/fixes/__pycache__/fix_unpacking.cpython-38.pyc,,
|
| 374 |
+
libpasteurize/fixes/feature_base.py,sha256=v7yLjBDBUPeNUc-YHGGlIsJDOQzFAM4Vo0RN5F1JHVU,1723
|
| 375 |
+
libpasteurize/fixes/fix_add_all__future__imports.py,sha256=mHet1LgbHn9GfgCYGNZXKo-rseDWreAvUcAjZwdgeTE,676
|
| 376 |
+
libpasteurize/fixes/fix_add_all_future_builtins.py,sha256=scfkY-Sz5j0yDtLYls2ENOcqEMPVxeDm9gFYYPINPB8,1269
|
| 377 |
+
libpasteurize/fixes/fix_add_future_standard_library_import.py,sha256=thTRbkBzy_SJjZ0bJteTp0sBTx8Wr69xFakH4styf7Y,663
|
| 378 |
+
libpasteurize/fixes/fix_annotations.py,sha256=VT_AorKY9AYWYZUZ17_CeUrJlEA7VGkwVLDQlwD1Bxo,1581
|
| 379 |
+
libpasteurize/fixes/fix_division.py,sha256=_TD_c5KniAYqEm11O7NJF0v2WEhYSNkRGcKG_94ZOas,904
|
| 380 |
+
libpasteurize/fixes/fix_features.py,sha256=NZn0n34_MYZpLNwyP1Tf51hOiN58Rg7A8tA9pK1S8-c,2675
|
| 381 |
+
libpasteurize/fixes/fix_fullargspec.py,sha256=VlZuIU6QNrClmRuvC4mtLICL3yMCi-RcGCnS9fD4b-Q,438
|
| 382 |
+
libpasteurize/fixes/fix_future_builtins.py,sha256=SlCK9I9u05m19Lr1wxlJxF8toZ5yu0yXBeDLxUN9_fw,1450
|
| 383 |
+
libpasteurize/fixes/fix_getcwd.py,sha256=uebvTvFboLqsROFCwdnzoP6ThziM0skz9TDXHoJcFsQ,873
|
| 384 |
+
libpasteurize/fixes/fix_imports.py,sha256=U4lIs_5Xp1qqM8mN72ieDkkIdiyALZFyCZsRC8ZmXlM,4944
|
| 385 |
+
libpasteurize/fixes/fix_imports2.py,sha256=bs2V5Yv0v_8xLx-lNj9kNEAK2dLYXUXkZ2hxECg01CU,8580
|
| 386 |
+
libpasteurize/fixes/fix_kwargs.py,sha256=NB_Ap8YJk-9ncoJRbOiPY_VMIigFgVB8m8AuY29DDhE,5991
|
| 387 |
+
libpasteurize/fixes/fix_memoryview.py,sha256=Fwayx_ezpr22tbJ0-QrKdJ-FZTpU-m7y78l1h_N4xxc,551
|
| 388 |
+
libpasteurize/fixes/fix_metaclass.py,sha256=IcE2KjaDG8jUR3FYXECzOC_cr2pr5r95W1NTbMrK8Wc,3260
|
| 389 |
+
libpasteurize/fixes/fix_newstyle.py,sha256=78sazKOHm9DUoMyW4VdvQpMXZhicbXzorVPRhBpSUrM,888
|
| 390 |
+
libpasteurize/fixes/fix_next.py,sha256=VHqcyORRNVqKJ51jJ1OkhwxHuXRgp8qaldyqcMvA4J0,1233
|
| 391 |
+
libpasteurize/fixes/fix_printfunction.py,sha256=NDIfqVmUJBG3H9E6nrnN0cWZK8ch9pL4F-nMexdsa38,401
|
| 392 |
+
libpasteurize/fixes/fix_raise.py,sha256=zQ_AcMsGmCbtKMgrxZGcHLYNscw6tqXFvHQxgqtNbU8,1099
|
| 393 |
+
libpasteurize/fixes/fix_raise_.py,sha256=9STp633frUfYASjYzqhwxx_MXePNmMhfJClowRj8FLY,1225
|
| 394 |
+
libpasteurize/fixes/fix_throw.py,sha256=_ZREVre-WttUvk4sWjrqUNqm9Q1uFaATECN0_-PXKbk,835
|
| 395 |
+
libpasteurize/fixes/fix_unpacking.py,sha256=eMqRe44Nfq8lo0YFL9oKW75dGARmBSmklj4BCS_q1Lo,5946
|
| 396 |
+
libpasteurize/main.py,sha256=dVHYTQQeJonuOFDNrenJZl-rKHgOQKRMPP1OqnJogWQ,8186
|
| 397 |
+
past/__init__.py,sha256=wIiXaAvXl3svDi-fzuy6HDD0VsuCVr4cnqnCr8XINGI,2918
|
| 398 |
+
past/__pycache__/__init__.cpython-38.pyc,,
|
| 399 |
+
past/builtins/__init__.py,sha256=7j_4OsUlN6q2eKr14do7mRQ1GwXRoXAMUR0A1fJpAls,1805
|
| 400 |
+
past/builtins/__pycache__/__init__.cpython-38.pyc,,
|
| 401 |
+
past/builtins/__pycache__/misc.cpython-38.pyc,,
|
| 402 |
+
past/builtins/__pycache__/noniterators.cpython-38.pyc,,
|
| 403 |
+
past/builtins/misc.py,sha256=nw62HVSxuAgT-Q2lD3lmgRB9zmFXopS14dZHEv5xpDQ,2627
|
| 404 |
+
past/builtins/noniterators.py,sha256=LtdELnd7KyYdXg7GkW25cgkEPUC0ggZ5AYMtDe9N95I,9370
|
| 405 |
+
past/translation/__init__.py,sha256=j2e6mLeK74KEICqH6P_-tpKqSNZoMwip2toThhSmKpU,17646
|
| 406 |
+
past/translation/__pycache__/__init__.cpython-38.pyc,,
|
| 407 |
+
past/types/__init__.py,sha256=RyJlgqg9uJ8oF-kJT9QlfhfdmhiMh3fShmtvd2CQycY,879
|
| 408 |
+
past/types/__pycache__/__init__.cpython-38.pyc,,
|
| 409 |
+
past/types/__pycache__/basestring.cpython-38.pyc,,
|
| 410 |
+
past/types/__pycache__/olddict.cpython-38.pyc,,
|
| 411 |
+
past/types/__pycache__/oldstr.cpython-38.pyc,,
|
| 412 |
+
past/types/basestring.py,sha256=qrImcr24wvdDCMvF9x0Tyx8S1lCt6GIwRvzuAmvg_Tg,728
|
| 413 |
+
past/types/olddict.py,sha256=0YtffZ55VY6AyQ_rwu4DZ4vcRsp6dz-dQzczeyN8hLk,2721
|
| 414 |
+
past/types/oldstr.py,sha256=J2sJPC5jWEdpqXPcFwJFNDKn51TKhi86PsLFmJtQr-M,4332
|
| 415 |
+
past/utils/__init__.py,sha256=e8l1sOfdiDJ3dkckBWLNWvC1ahC5BX5haHC2TGdNgA8,2633
|
| 416 |
+
past/utils/__pycache__/__init__.cpython-38.pyc,,
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/REQUESTED
ADDED
|
File without changes
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: bdist_wheel (0.36.2)
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
| 5 |
+
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/future-0.18.2.dist-info/top_level.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
future
|
| 2 |
+
libfuturize
|
| 3 |
+
libpasteurize
|
| 4 |
+
past
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
|
| 3 |
+
# Source of truth for Hydra's version
|
| 4 |
+
__version__ = "1.2.0"
|
| 5 |
+
from hydra import utils
|
| 6 |
+
from hydra.errors import MissingConfigException
|
| 7 |
+
from hydra.main import main
|
| 8 |
+
from hydra.types import TaskFunction
|
| 9 |
+
|
| 10 |
+
from .compose import compose
|
| 11 |
+
from .initialize import initialize, initialize_config_dir, initialize_config_module
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"__version__",
|
| 15 |
+
"MissingConfigException",
|
| 16 |
+
"main",
|
| 17 |
+
"utils",
|
| 18 |
+
"TaskFunction",
|
| 19 |
+
"compose",
|
| 20 |
+
"initialize",
|
| 21 |
+
"initialize_config_module",
|
| 22 |
+
"initialize_config_dir",
|
| 23 |
+
]
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/compose.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
from textwrap import dedent
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
|
| 5 |
+
from omegaconf import DictConfig, OmegaConf, open_dict
|
| 6 |
+
|
| 7 |
+
from hydra import version
|
| 8 |
+
from hydra.core.global_hydra import GlobalHydra
|
| 9 |
+
from hydra.types import RunMode
|
| 10 |
+
|
| 11 |
+
from ._internal.deprecation_warning import deprecation_warning
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def compose(
|
| 15 |
+
config_name: Optional[str] = None,
|
| 16 |
+
overrides: List[str] = [],
|
| 17 |
+
return_hydra_config: bool = False,
|
| 18 |
+
strict: Optional[bool] = None,
|
| 19 |
+
) -> DictConfig:
|
| 20 |
+
"""
|
| 21 |
+
:param config_name: the name of the config
|
| 22 |
+
(usually the file name without the .yaml extension)
|
| 23 |
+
:param overrides: list of overrides for config file
|
| 24 |
+
:param return_hydra_config: True to return the hydra config node in the result
|
| 25 |
+
:param strict: DEPRECATED. If false, returned config has struct mode disabled.
|
| 26 |
+
:return: the composed config
|
| 27 |
+
"""
|
| 28 |
+
assert (
|
| 29 |
+
GlobalHydra().is_initialized()
|
| 30 |
+
), "GlobalHydra is not initialized, use @hydra.main() or call one of the hydra initialization methods first"
|
| 31 |
+
|
| 32 |
+
gh = GlobalHydra.instance()
|
| 33 |
+
assert gh.hydra is not None
|
| 34 |
+
cfg = gh.hydra.compose_config(
|
| 35 |
+
config_name=config_name,
|
| 36 |
+
overrides=overrides,
|
| 37 |
+
run_mode=RunMode.RUN,
|
| 38 |
+
from_shell=False,
|
| 39 |
+
with_log_configuration=False,
|
| 40 |
+
)
|
| 41 |
+
assert isinstance(cfg, DictConfig)
|
| 42 |
+
|
| 43 |
+
if not return_hydra_config:
|
| 44 |
+
if "hydra" in cfg:
|
| 45 |
+
with open_dict(cfg):
|
| 46 |
+
del cfg["hydra"]
|
| 47 |
+
|
| 48 |
+
if strict is not None:
|
| 49 |
+
if version.base_at_least("1.2"):
|
| 50 |
+
raise TypeError("got an unexpected 'strict' argument")
|
| 51 |
+
else:
|
| 52 |
+
deprecation_warning(
|
| 53 |
+
dedent(
|
| 54 |
+
"""
|
| 55 |
+
The strict flag in the compose API is deprecated.
|
| 56 |
+
See https://hydra.cc/docs/upgrades/0.11_to_1.0/strict_mode_flag_deprecated for more info.
|
| 57 |
+
"""
|
| 58 |
+
)
|
| 59 |
+
)
|
| 60 |
+
OmegaConf.set_struct(cfg, strict)
|
| 61 |
+
|
| 62 |
+
return cfg
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/errors.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
from typing import Optional, Sequence
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class HydraException(Exception):
|
| 6 |
+
...
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class CompactHydraException(HydraException):
|
| 10 |
+
...
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class OverrideParseException(CompactHydraException):
|
| 14 |
+
def __init__(self, override: str, message: str) -> None:
|
| 15 |
+
super(OverrideParseException, self).__init__(message)
|
| 16 |
+
self.override = override
|
| 17 |
+
self.message = message
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class InstantiationException(CompactHydraException):
|
| 21 |
+
...
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ConfigCompositionException(CompactHydraException):
|
| 25 |
+
...
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class SearchPathException(CompactHydraException):
|
| 29 |
+
...
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class MissingConfigException(IOError, ConfigCompositionException):
|
| 33 |
+
def __init__(
|
| 34 |
+
self,
|
| 35 |
+
message: str,
|
| 36 |
+
missing_cfg_file: Optional[str],
|
| 37 |
+
options: Optional[Sequence[str]] = None,
|
| 38 |
+
) -> None:
|
| 39 |
+
super(MissingConfigException, self).__init__(message)
|
| 40 |
+
self.missing_cfg_file = missing_cfg_file
|
| 41 |
+
self.options = options
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class HydraDeprecationError(HydraException):
|
| 45 |
+
...
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/initialize.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
import copy
|
| 3 |
+
import os
|
| 4 |
+
from textwrap import dedent
|
| 5 |
+
from typing import Any, Optional
|
| 6 |
+
|
| 7 |
+
from hydra import version
|
| 8 |
+
from hydra._internal.deprecation_warning import deprecation_warning
|
| 9 |
+
from hydra._internal.hydra import Hydra
|
| 10 |
+
from hydra._internal.utils import (
|
| 11 |
+
create_config_search_path,
|
| 12 |
+
detect_calling_file_or_module_from_stack_frame,
|
| 13 |
+
detect_task_name,
|
| 14 |
+
)
|
| 15 |
+
from hydra.core.global_hydra import GlobalHydra
|
| 16 |
+
from hydra.core.singleton import Singleton
|
| 17 |
+
from hydra.errors import HydraException
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_gh_backup() -> Any:
|
| 21 |
+
if GlobalHydra in Singleton._instances:
|
| 22 |
+
return copy.deepcopy(Singleton._instances[GlobalHydra])
|
| 23 |
+
else:
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def restore_gh_from_backup(_gh_backup: Any) -> Any:
|
| 28 |
+
if _gh_backup is None:
|
| 29 |
+
del Singleton._instances[GlobalHydra]
|
| 30 |
+
else:
|
| 31 |
+
Singleton._instances[GlobalHydra] = _gh_backup
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
_UNSPECIFIED_: Any = object()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class initialize:
|
| 38 |
+
"""
|
| 39 |
+
Initializes Hydra and add the config_path to the config search path.
|
| 40 |
+
config_path is relative to the parent of the caller.
|
| 41 |
+
Hydra detects the caller type automatically at runtime.
|
| 42 |
+
|
| 43 |
+
Supported callers:
|
| 44 |
+
- Python scripts
|
| 45 |
+
- Python modules
|
| 46 |
+
- Unit tests
|
| 47 |
+
- Jupyter notebooks.
|
| 48 |
+
:param config_path: path relative to the parent of the caller
|
| 49 |
+
:param job_name: the value for hydra.job.name (By default it is automatically detected based on the caller)
|
| 50 |
+
:param caller_stack_depth: stack depth of the caller, defaults to 1 (direct caller).
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def __init__(
|
| 54 |
+
self,
|
| 55 |
+
config_path: Optional[str] = _UNSPECIFIED_,
|
| 56 |
+
version_base: Optional[str] = _UNSPECIFIED_,
|
| 57 |
+
job_name: Optional[str] = None,
|
| 58 |
+
caller_stack_depth: int = 1,
|
| 59 |
+
) -> None:
|
| 60 |
+
self._gh_backup = get_gh_backup()
|
| 61 |
+
|
| 62 |
+
version.setbase(version_base)
|
| 63 |
+
|
| 64 |
+
if config_path is _UNSPECIFIED_:
|
| 65 |
+
if version.base_at_least("1.2"):
|
| 66 |
+
config_path = None
|
| 67 |
+
elif version_base is _UNSPECIFIED_:
|
| 68 |
+
url = "https://hydra.cc/docs/next/upgrades/1.0_to_1.1/changes_to_hydra_main_config_path"
|
| 69 |
+
deprecation_warning(
|
| 70 |
+
message=dedent(
|
| 71 |
+
f"""\
|
| 72 |
+
config_path is not specified in hydra.initialize().
|
| 73 |
+
See {url} for more information."""
|
| 74 |
+
),
|
| 75 |
+
stacklevel=2,
|
| 76 |
+
)
|
| 77 |
+
config_path = "."
|
| 78 |
+
else:
|
| 79 |
+
config_path = "."
|
| 80 |
+
|
| 81 |
+
if config_path is not None and os.path.isabs(config_path):
|
| 82 |
+
raise HydraException("config_path in initialize() must be relative")
|
| 83 |
+
calling_file, calling_module = detect_calling_file_or_module_from_stack_frame(
|
| 84 |
+
caller_stack_depth + 1
|
| 85 |
+
)
|
| 86 |
+
if job_name is None:
|
| 87 |
+
job_name = detect_task_name(
|
| 88 |
+
calling_file=calling_file, calling_module=calling_module
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
Hydra.create_main_hydra_file_or_module(
|
| 92 |
+
calling_file=calling_file,
|
| 93 |
+
calling_module=calling_module,
|
| 94 |
+
config_path=config_path,
|
| 95 |
+
job_name=job_name,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
def __enter__(self, *args: Any, **kwargs: Any) -> None:
|
| 99 |
+
...
|
| 100 |
+
|
| 101 |
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
| 102 |
+
restore_gh_from_backup(self._gh_backup)
|
| 103 |
+
|
| 104 |
+
def __repr__(self) -> str:
|
| 105 |
+
return "hydra.initialize()"
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class initialize_config_module:
|
| 109 |
+
"""
|
| 110 |
+
Initializes Hydra and add the config_module to the config search path.
|
| 111 |
+
The config module must be importable (an __init__.py must exist at its top level)
|
| 112 |
+
:param config_module: absolute module name, for example "foo.bar.conf".
|
| 113 |
+
:param job_name: the value for hydra.job.name (default is 'app')
|
| 114 |
+
"""
|
| 115 |
+
|
| 116 |
+
def __init__(
|
| 117 |
+
self,
|
| 118 |
+
config_module: str,
|
| 119 |
+
version_base: Optional[str] = _UNSPECIFIED_,
|
| 120 |
+
job_name: str = "app",
|
| 121 |
+
):
|
| 122 |
+
self._gh_backup = get_gh_backup()
|
| 123 |
+
|
| 124 |
+
version.setbase(version_base)
|
| 125 |
+
|
| 126 |
+
Hydra.create_main_hydra_file_or_module(
|
| 127 |
+
calling_file=None,
|
| 128 |
+
calling_module=f"{config_module}.{job_name}",
|
| 129 |
+
config_path=None,
|
| 130 |
+
job_name=job_name,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
def __enter__(self, *args: Any, **kwargs: Any) -> None:
|
| 134 |
+
...
|
| 135 |
+
|
| 136 |
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
| 137 |
+
restore_gh_from_backup(self._gh_backup)
|
| 138 |
+
|
| 139 |
+
def __repr__(self) -> str:
|
| 140 |
+
return "hydra.initialize_config_module()"
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class initialize_config_dir:
|
| 144 |
+
"""
|
| 145 |
+
Initializes Hydra and add an absolute config dir to the to the config search path.
|
| 146 |
+
The config_dir is always a path on the file system and is must be an absolute path.
|
| 147 |
+
Relative paths will result in an error.
|
| 148 |
+
:param config_dir: absolute file system path
|
| 149 |
+
:param job_name: the value for hydra.job.name (default is 'app')
|
| 150 |
+
"""
|
| 151 |
+
|
| 152 |
+
def __init__(
|
| 153 |
+
self,
|
| 154 |
+
config_dir: str,
|
| 155 |
+
version_base: Optional[str] = _UNSPECIFIED_,
|
| 156 |
+
job_name: str = "app",
|
| 157 |
+
) -> None:
|
| 158 |
+
self._gh_backup = get_gh_backup()
|
| 159 |
+
|
| 160 |
+
version.setbase(version_base)
|
| 161 |
+
|
| 162 |
+
# Relative here would be interpreted as relative to cwd, which - depending on when it run
|
| 163 |
+
# may have unexpected meaning. best to force an absolute path to avoid confusion.
|
| 164 |
+
# Can consider using hydra.utils.to_absolute_path() to convert it at a future point if there is demand.
|
| 165 |
+
if not os.path.isabs(config_dir):
|
| 166 |
+
raise HydraException(
|
| 167 |
+
"initialize_config_dir() requires an absolute config_dir as input"
|
| 168 |
+
)
|
| 169 |
+
csp = create_config_search_path(search_path_dir=config_dir)
|
| 170 |
+
Hydra.create_main_hydra2(task_name=job_name, config_search_path=csp)
|
| 171 |
+
|
| 172 |
+
def __enter__(self, *args: Any, **kwargs: Any) -> None:
|
| 173 |
+
...
|
| 174 |
+
|
| 175 |
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
| 176 |
+
restore_gh_from_backup(self._gh_backup)
|
| 177 |
+
|
| 178 |
+
def __repr__(self) -> str:
|
| 179 |
+
return "hydra.initialize_config_dir()"
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/main.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
import copy
|
| 3 |
+
import functools
|
| 4 |
+
import pickle
|
| 5 |
+
import warnings
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from textwrap import dedent
|
| 8 |
+
from typing import Any, Callable, List, Optional
|
| 9 |
+
|
| 10 |
+
from omegaconf import DictConfig, open_dict, read_write
|
| 11 |
+
|
| 12 |
+
from . import version
|
| 13 |
+
from ._internal.deprecation_warning import deprecation_warning
|
| 14 |
+
from ._internal.utils import _run_hydra, get_args_parser
|
| 15 |
+
from .core.hydra_config import HydraConfig
|
| 16 |
+
from .core.utils import _flush_loggers, configure_log
|
| 17 |
+
from .types import TaskFunction
|
| 18 |
+
|
| 19 |
+
_UNSPECIFIED_: Any = object()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_rerun_conf(file_path: str, overrides: List[str]) -> DictConfig:
|
| 23 |
+
msg = "Experimental rerun CLI option, other command line args are ignored."
|
| 24 |
+
warnings.warn(msg, UserWarning)
|
| 25 |
+
file = Path(file_path)
|
| 26 |
+
if not file.exists():
|
| 27 |
+
raise ValueError(f"File {file} does not exist!")
|
| 28 |
+
|
| 29 |
+
if len(overrides) > 0:
|
| 30 |
+
msg = "Config overrides are not supported as of now."
|
| 31 |
+
warnings.warn(msg, UserWarning)
|
| 32 |
+
|
| 33 |
+
with open(str(file), "rb") as input:
|
| 34 |
+
config = pickle.load(input) # nosec
|
| 35 |
+
configure_log(config.hydra.job_logging, config.hydra.verbose)
|
| 36 |
+
HydraConfig.instance().set_config(config)
|
| 37 |
+
task_cfg = copy.deepcopy(config)
|
| 38 |
+
with read_write(task_cfg):
|
| 39 |
+
with open_dict(task_cfg):
|
| 40 |
+
del task_cfg["hydra"]
|
| 41 |
+
assert isinstance(task_cfg, DictConfig)
|
| 42 |
+
return task_cfg
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def main(
|
| 46 |
+
config_path: Optional[str] = _UNSPECIFIED_,
|
| 47 |
+
config_name: Optional[str] = None,
|
| 48 |
+
version_base: Optional[str] = _UNSPECIFIED_,
|
| 49 |
+
) -> Callable[[TaskFunction], Any]:
|
| 50 |
+
"""
|
| 51 |
+
:param config_path: The config path, a directory relative to the declaring python file.
|
| 52 |
+
If config_path is None no directory is added to the Config search path.
|
| 53 |
+
:param config_name: The name of the config (usually the file name without the .yaml extension)
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
version.setbase(version_base)
|
| 57 |
+
|
| 58 |
+
if config_path is _UNSPECIFIED_:
|
| 59 |
+
if version.base_at_least("1.2"):
|
| 60 |
+
config_path = None
|
| 61 |
+
elif version_base is _UNSPECIFIED_:
|
| 62 |
+
url = "https://hydra.cc/docs/next/upgrades/1.0_to_1.1/changes_to_hydra_main_config_path"
|
| 63 |
+
deprecation_warning(
|
| 64 |
+
message=dedent(
|
| 65 |
+
f"""
|
| 66 |
+
config_path is not specified in @hydra.main().
|
| 67 |
+
See {url} for more information."""
|
| 68 |
+
),
|
| 69 |
+
stacklevel=2,
|
| 70 |
+
)
|
| 71 |
+
config_path = "."
|
| 72 |
+
else:
|
| 73 |
+
config_path = "."
|
| 74 |
+
|
| 75 |
+
def main_decorator(task_function: TaskFunction) -> Callable[[], None]:
|
| 76 |
+
@functools.wraps(task_function)
|
| 77 |
+
def decorated_main(cfg_passthrough: Optional[DictConfig] = None) -> Any:
|
| 78 |
+
if cfg_passthrough is not None:
|
| 79 |
+
return task_function(cfg_passthrough)
|
| 80 |
+
else:
|
| 81 |
+
args_parser = get_args_parser()
|
| 82 |
+
args = args_parser.parse_args()
|
| 83 |
+
if args.experimental_rerun is not None:
|
| 84 |
+
cfg = _get_rerun_conf(args.experimental_rerun, args.overrides)
|
| 85 |
+
task_function(cfg)
|
| 86 |
+
_flush_loggers()
|
| 87 |
+
else:
|
| 88 |
+
# no return value from run_hydra() as it may sometime actually run the task_function
|
| 89 |
+
# multiple times (--multirun)
|
| 90 |
+
_run_hydra(
|
| 91 |
+
args=args,
|
| 92 |
+
args_parser=args_parser,
|
| 93 |
+
task_function=task_function,
|
| 94 |
+
config_path=config_path,
|
| 95 |
+
config_name=config_name,
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return decorated_main
|
| 99 |
+
|
| 100 |
+
return main_decorator
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/py.typed
ADDED
|
File without changes
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/types.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from enum import Enum
|
| 4 |
+
from typing import TYPE_CHECKING, Any, Callable
|
| 5 |
+
|
| 6 |
+
from omegaconf import MISSING
|
| 7 |
+
|
| 8 |
+
from hydra import version
|
| 9 |
+
|
| 10 |
+
from ._internal.deprecation_warning import deprecation_warning
|
| 11 |
+
|
| 12 |
+
TaskFunction = Callable[[Any], Any]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
if TYPE_CHECKING:
|
| 16 |
+
from hydra._internal.callbacks import Callbacks
|
| 17 |
+
from hydra.core.config_loader import ConfigLoader
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class HydraContext:
|
| 22 |
+
config_loader: "ConfigLoader"
|
| 23 |
+
callbacks: "Callbacks"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@dataclass
|
| 27 |
+
class TargetConf:
|
| 28 |
+
"""
|
| 29 |
+
This class is going away in Hydra 1.2.
|
| 30 |
+
You should no longer extend it or annotate with it.
|
| 31 |
+
instantiate will work correctly if you pass in a DictConfig object or any dataclass that has the
|
| 32 |
+
_target_ attribute.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
_target_: str = MISSING
|
| 36 |
+
|
| 37 |
+
def __post_init__(self) -> None:
|
| 38 |
+
if version.base_at_least("1.2"):
|
| 39 |
+
raise TypeError("TargetConf is unsupported since Hydra 1.2")
|
| 40 |
+
else:
|
| 41 |
+
msg = "\nTargetConf is deprecated since Hydra 1.1 and will be removed in Hydra 1.2."
|
| 42 |
+
deprecation_warning(message=msg)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class RunMode(Enum):
|
| 46 |
+
RUN = 1
|
| 47 |
+
MULTIRUN = 2
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ConvertMode(Enum):
|
| 51 |
+
"""ConvertMode for instantiate, controls return type.
|
| 52 |
+
|
| 53 |
+
A config is either config or instance-like (`_target_` field).
|
| 54 |
+
|
| 55 |
+
If instance-like, instantiate resolves the callable (class or
|
| 56 |
+
function) and returns the result of the call on the rest of the
|
| 57 |
+
parameters.
|
| 58 |
+
|
| 59 |
+
If "none", config-like configs will be kept as is.
|
| 60 |
+
|
| 61 |
+
If "partial", config-like configs will be converted to native python
|
| 62 |
+
containers (list and dict), unless they are structured configs (
|
| 63 |
+
dataclasses or attr instances).
|
| 64 |
+
|
| 65 |
+
If "all", config-like configs will all be converted to native python
|
| 66 |
+
containers (list and dict).
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
# Use DictConfig/ListConfig
|
| 70 |
+
NONE = "none"
|
| 71 |
+
# Convert the OmegaConf config to primitive container, Structured Configs are preserved
|
| 72 |
+
PARTIAL = "partial"
|
| 73 |
+
# Fully convert the OmegaConf config to primitive containers (dict, list and primitives).
|
| 74 |
+
ALL = "all"
|
| 75 |
+
|
| 76 |
+
def __eq__(self, other: Any) -> Any:
|
| 77 |
+
if isinstance(other, ConvertMode):
|
| 78 |
+
return other.value == self.value
|
| 79 |
+
elif isinstance(other, str):
|
| 80 |
+
return other.upper() == self.name.upper()
|
| 81 |
+
else:
|
| 82 |
+
return NotImplemented
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/utils.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
|
| 3 |
+
import logging.config
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Callable
|
| 7 |
+
|
| 8 |
+
import hydra._internal.instantiate._instantiate2
|
| 9 |
+
import hydra.types
|
| 10 |
+
from hydra._internal.utils import _locate
|
| 11 |
+
from hydra.core.hydra_config import HydraConfig
|
| 12 |
+
|
| 13 |
+
log = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
# Instantiation related symbols
|
| 16 |
+
instantiate = hydra._internal.instantiate._instantiate2.instantiate
|
| 17 |
+
call = instantiate
|
| 18 |
+
ConvertMode = hydra.types.ConvertMode
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def get_class(path: str) -> type:
|
| 22 |
+
try:
|
| 23 |
+
cls = _locate(path)
|
| 24 |
+
if not isinstance(cls, type):
|
| 25 |
+
raise ValueError(
|
| 26 |
+
f"Located non-class of type '{type(cls).__name__}'"
|
| 27 |
+
+ f" while loading '{path}'"
|
| 28 |
+
)
|
| 29 |
+
return cls
|
| 30 |
+
except Exception as e:
|
| 31 |
+
log.error(f"Error initializing class at {path}: {e}")
|
| 32 |
+
raise e
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def get_method(path: str) -> Callable[..., Any]:
|
| 36 |
+
try:
|
| 37 |
+
obj = _locate(path)
|
| 38 |
+
if not callable(obj):
|
| 39 |
+
raise ValueError(
|
| 40 |
+
f"Located non-callable of type '{type(obj).__name__}'"
|
| 41 |
+
+ f" while loading '{path}'"
|
| 42 |
+
)
|
| 43 |
+
cl: Callable[..., Any] = obj
|
| 44 |
+
return cl
|
| 45 |
+
except Exception as e:
|
| 46 |
+
log.error(f"Error getting callable at {path} : {e}")
|
| 47 |
+
raise e
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# Alias for get_method
|
| 51 |
+
get_static_method = get_method
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_original_cwd() -> str:
|
| 55 |
+
"""
|
| 56 |
+
:return: the original working directory the Hydra application was launched from
|
| 57 |
+
"""
|
| 58 |
+
if not HydraConfig.initialized():
|
| 59 |
+
raise ValueError(
|
| 60 |
+
"get_original_cwd() must only be used after HydraConfig is initialized"
|
| 61 |
+
)
|
| 62 |
+
ret = HydraConfig.get().runtime.cwd
|
| 63 |
+
assert ret is not None and isinstance(ret, str)
|
| 64 |
+
return ret
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def to_absolute_path(path: str) -> str:
|
| 68 |
+
"""
|
| 69 |
+
converts the specified path to be absolute path.
|
| 70 |
+
if the input path is relative, it's interpreted as relative to the original working directory
|
| 71 |
+
if it's absolute, it's returned as is
|
| 72 |
+
:param path: path to convert
|
| 73 |
+
:return:
|
| 74 |
+
"""
|
| 75 |
+
p = Path(path)
|
| 76 |
+
if not HydraConfig.initialized():
|
| 77 |
+
base = Path(os.getcwd())
|
| 78 |
+
else:
|
| 79 |
+
base = Path(get_original_cwd())
|
| 80 |
+
if p.is_absolute():
|
| 81 |
+
ret = p
|
| 82 |
+
else:
|
| 83 |
+
ret = base / p
|
| 84 |
+
return str(ret)
|
my_container_sandbox/workspace/anaconda3/lib/python3.8/site-packages/hydra/version.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
|
| 2 |
+
|
| 3 |
+
# Source of truth for Hydra's version
|
| 4 |
+
|
| 5 |
+
from textwrap import dedent
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
|
| 8 |
+
from packaging.version import Version
|
| 9 |
+
|
| 10 |
+
from . import __version__
|
| 11 |
+
from ._internal.deprecation_warning import deprecation_warning
|
| 12 |
+
from .core.singleton import Singleton
|
| 13 |
+
from .errors import HydraException
|
| 14 |
+
|
| 15 |
+
_UNSPECIFIED_: Any = object()
|
| 16 |
+
|
| 17 |
+
__compat_version__: Version = Version("1.1")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class VersionBase(metaclass=Singleton):
|
| 21 |
+
def __init__(self) -> None:
|
| 22 |
+
self.version_base: Optional[Version] = _UNSPECIFIED_
|
| 23 |
+
|
| 24 |
+
def setbase(self, version: "Version") -> None:
|
| 25 |
+
assert isinstance(
|
| 26 |
+
version, Version
|
| 27 |
+
), f"Unexpected Version type : {type(version)}"
|
| 28 |
+
self.version_base = version
|
| 29 |
+
|
| 30 |
+
def getbase(self) -> Optional[Version]:
|
| 31 |
+
return self.version_base
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
def instance(*args: Any, **kwargs: Any) -> "VersionBase":
|
| 35 |
+
return Singleton.instance(VersionBase, *args, **kwargs) # type: ignore
|
| 36 |
+
|
| 37 |
+
@staticmethod
|
| 38 |
+
def set_instance(instance: "VersionBase") -> None:
|
| 39 |
+
assert isinstance(instance, VersionBase)
|
| 40 |
+
Singleton._instances[VersionBase] = instance # type: ignore
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _get_version(ver: str) -> Version:
|
| 44 |
+
# Only consider major.minor as packaging will compare "1.2.0.dev2" < "1.2"
|
| 45 |
+
pver = Version(ver)
|
| 46 |
+
return Version(f"{pver.major}.{pver.minor}")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def base_at_least(ver: str) -> bool:
|
| 50 |
+
_version_base = VersionBase.instance().getbase()
|
| 51 |
+
if type(_version_base) is type(_UNSPECIFIED_):
|
| 52 |
+
VersionBase.instance().setbase(__compat_version__)
|
| 53 |
+
_version_base = __compat_version__
|
| 54 |
+
assert isinstance(_version_base, Version)
|
| 55 |
+
return _version_base >= _get_version(ver)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def getbase() -> Optional[Version]:
|
| 59 |
+
return VersionBase.instance().getbase()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def setbase(ver: Any) -> None:
|
| 63 |
+
if type(ver) is type(_UNSPECIFIED_):
|
| 64 |
+
deprecation_warning(
|
| 65 |
+
message=dedent(
|
| 66 |
+
f"""
|
| 67 |
+
The version_base parameter is not specified.
|
| 68 |
+
Please specify a compatability version level, or None.
|
| 69 |
+
Will assume defaults for version {__compat_version__}"""
|
| 70 |
+
),
|
| 71 |
+
stacklevel=3,
|
| 72 |
+
)
|
| 73 |
+
_version_base = __compat_version__
|
| 74 |
+
elif ver is None:
|
| 75 |
+
_version_base = _get_version(__version__)
|
| 76 |
+
else:
|
| 77 |
+
_version_base = _get_version(ver)
|
| 78 |
+
if _version_base < __compat_version__:
|
| 79 |
+
raise HydraException(f'version_base must be >= "{__compat_version__}"')
|
| 80 |
+
VersionBase.instance().setbase(_version_base)
|