├── requirements.txt ├── tests ├── __init__.py └── test_memory_tempfile.py ├── memory_tempfile ├── __init__.py └── memory_tempfile.py ├── dev-requirements.txt ├── .gitignore ├── pyproject.toml ├── LICENSE.txt ├── README.md └── poetry.lock /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /memory_tempfile/__init__.py: -------------------------------------------------------------------------------- 1 | from memory_tempfile.memory_tempfile import MemoryTempfile, MEM_BASED_FS, SUITABLE_PATHS 2 | 3 | __version__ = '0.1.0' 4 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | atomicwrites==1.3.0; sys_platform == "win32" 2 | attrs==19.3.0 3 | colorama==0.4.3; sys_platform == "win32" 4 | importlib-metadata==1.4.0; python_version < "3.8" 5 | more-itertools==8.1.0 6 | packaging==20.0 7 | pluggy==0.13.1 8 | py==1.8.1 9 | py-cpuinfo==5.0.0 10 | pyparsing==2.4.6 11 | pytest==5.3.3 12 | pytest-benchmark==3.2.3 13 | six==1.14.0 14 | wcwidth==0.1.8 15 | zipp==2.0.0; python_version < "3.8" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.tex 3 | *.log 4 | *.pdf 5 | new_release.sh 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | _build 13 | .cache 14 | *.so 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | .pytest_cache 23 | 24 | .DS_Store 25 | .idea/* 26 | .python-version 27 | .vscode/* 28 | 29 | /test.py 30 | /test_*.* 31 | 32 | .venv 33 | /releases/* 34 | 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "memory-tempfile" 3 | version = "2.2.3" 4 | description = "Helper functions to identify and use paths on the OS (Linux-only for now) where RAM-based tempfiles can be created." 5 | authors = ["mbello "] 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/mbello/memory-tempfile" 9 | homepage = "https://github.com/mbello/memory-tempfile" 10 | keywords = ['tempfile in RAM', 'tempfile in memory', 'file in memory', 'tempfs', 'ramdisk', '/dev/shm', '/run/user'] 11 | 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.6" 15 | 16 | [tool.poetry.dev-dependencies] 17 | pytest = "^5.3.3" 18 | pytest-benchmark = "^3.2.3" 19 | 20 | [build-system] 21 | requires = ["poetry>=0.12"] 22 | build-backend = "poetry.masonry.api" 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marcelo Bello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_memory_tempfile.py: -------------------------------------------------------------------------------- 1 | from memory_tempfile import __version__ 2 | 3 | 4 | def test_version(): 5 | assert __version__ == '0.1.0' 6 | 7 | 8 | def example1(): 9 | from memory_tempfile import MemoryTempfile 10 | 11 | tempfile = MemoryTempfile() 12 | 13 | with tempfile.TemporaryDirectory() as td: 14 | # work as usual 15 | pass 16 | 17 | 18 | def example2(): 19 | # We now do not want to use /dev/shm or /run/shm and no ramfs paths 20 | # If /run/user/{uid} is available, we prefer it to /tmp 21 | # And we want to try /var/run as a last resort 22 | # If all fails, fallback to platform's tmp dir 23 | 24 | from memory_tempfile import MemoryTempfile 25 | 26 | # By the way, all paths with string {uid} will have it replaced with the user id 27 | tempfile = MemoryTempfile(preferred_paths=['/run/user/{uid}'], remove_paths=['/dev/shm', '/run/shm'], 28 | additional_paths=['/var/run'], filesystem_types=['tmpfs'], fallback=True) 29 | 30 | if tempfile.found_mem_tempdir(): 31 | print('We could use any of the followig paths: {}'.format(tempfile.get_usable_mem_tempdir_paths())) 32 | print('And we are using now: {}'.format(tempfile.gettempdir())) 33 | 34 | with tempfile.NamedTemporaryFile() as ntf: 35 | # use it as usual... 36 | pass 37 | 38 | 39 | example1() 40 | example2() 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Often there is a need to temporarily save a file to 'disk' for the consumption of external tools. Or maybe you can pipe some input info to an external tool 4 | but has no way of forcing such external tool to pipe its output straight to your software: **it wants to write a file to disk**. 5 | Disk operations are slow and if repeated too often can shorten the lifespan of underlying media. 6 | 7 | On Linux, most distributions offer a /tmp directory BUT it is usually on physical media. However, modern distributions often offer at least two 8 | places where one can safely create temporary files in RAM: /dev/run/, /run/shm and /dev/shm 9 | 10 | /dev/run/ is ideal for your temporary files. It is writable and readable only by your user. 11 | /dev/shm is usually world-readable and world-writable (just like /tmp), it is often used for IPC (inter process communication) and can serve well as a temporary RAM-based tempdir. 12 | 13 | This module is very simple and tries not to reinvent the wheel. It will check /tmp to see if it in a ramdisk or not. And it will also check 14 | if you have other options where to place your temporary files/dirs on a memory-based file system like tmpfs or ramfs. 15 | 16 | Once you know the suitable path on a memory-based file system where you can have your files, you are well served by python's builtin modules and external packages like pathlib 17 | or pyfilesystem2 to move on to do your things. 18 | 19 | **To know more, I recommend the following links:** 20 | https://unix.stackexchange.com/questions/162900/what-is-this-folder-run-user-1000 21 | https://superuser.com/questions/45342/when-should-i-use-dev-shm-and-when-should-i-use-tmp 22 | 23 | 24 | # API 25 | This module searches for paths hosted on filesystems of type belonging to MEM_BASED_FS=['tmpfs', 'ramfs'] 26 | Paths in SUITABLE_PATHS=['/tmp', '/run/user/{uid}', '/run/shm', '/dev/shm'] are searched and the first path found that exists and is stored on a filesystem whose type belongs to MEM_BASED_FS will be used as the tempdir. 27 | If no suitable path is found, then if fallback = True, we will fallback to default tempdir (as determined by tempfile stdlib). If fallback is a path, then we will default to it. 28 | If fallback is false, a RunTimeError exception is raised. 29 | 30 | The MemoryTempfile constructor has arguments that let you change how the algorithm works. 31 | You can change the order of paths (with 'preferred_paths'), add new paths to the search (with 'preferred_paths' and/or with 'additional_paths') and you can exclude certain paths from SUITABLE_PATHS (with removed_paths). All paths containing the string {uid} will have it replaced by the user id. 32 | You can change the filesystem types you accept (with filesystem_types) and specify whether or not to fallback to a vanilla tempdir as a last resort. 33 | 34 | Then, all methods available from tempfile stdlib are available through MemoryTempfile. 35 | 36 | # The constructor: 37 | 38 | **Here is the list of accepted parameters:** 39 | - preferred_paths: list = None 40 | - remove_paths: list or bool = None 41 | - additional_paths: list = None 42 | - filesystem_types: list = None 43 | - fallback: str or bool = None 44 | 45 | The path list that will be searched from first to last item will be constructed using the algorithm: 46 | 47 | paths = preferred_paths + (SUITABLE_PATHS - remove_paths) + additional_paths 48 | 49 | If remove_paths is boolean 'true', SUITABLE_PATHS will be eliminated, this is a way for you to take complete control of the path list 50 | that will be used without relying on this package's hardcoded constants. 51 | 52 | The only other hardcoded constant MEM_BASED_FS=['tmpfs', 'ramfs'] will not be used at all if you pass your own 'filesystem_types' argument. 53 | By the way, if you wish to add other file system types, you must match what Linux uses in /proc/self/mountinfo (at the 9th column). 54 | 55 | # Requirements 56 | 57 | - Python 3 58 | - Works only on Linux 59 | - Compatible with chroot and/or namespaces, needs access to /proc/self/mountinfo 60 | 61 | # Usage 62 | 63 | ## Example 1: 64 | 65 | from memory_tempfile import MemoryTempfile 66 | 67 | tempfile = MemoryTempfile() 68 | 69 | with tempfile.TemporaryFile() as tf: 70 | # as usual... 71 | 72 | ## Example 2: 73 | 74 | # We now do not want to use /dev/shm or /run/shm and no ramfs paths 75 | # If /run/user/{uid} is available, we prefer it to /tmp 76 | # And we want to try /var/run as a last resort 77 | # If all fails, fallback to platform's tmp dir 78 | 79 | from memory_tempfile import MemoryTempfile 80 | import memory_tempfile 81 | 82 | # By the way, all paths with string {uid} will have it replaced with the user id 83 | tempfile = MemoryTempfile(preferred_paths=['/run/user/{uid}'], remove_paths=['/dev/shm', '/run/shm'], 84 | additional_paths=['/var/run'], filesystem_types=['tmpfs'], fallback=True) 85 | 86 | if tempfile.found_mem_tempdir(): 87 | print('We could use any of the following paths: {}'.format(tempfile.get_usable_mem_tempdir_paths())) 88 | print('And we are using now: {}'.format(tempfile.gettempdir())) 89 | 90 | with tempfile.NamedTemporaryFile() as ntf: 91 | # use it as usual... 92 | pass 93 | -------------------------------------------------------------------------------- /memory_tempfile/memory_tempfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import tempfile 4 | import platform 5 | from collections import OrderedDict 6 | 7 | MEM_BASED_FS = ['tmpfs', 'ramfs'] 8 | SUITABLE_PATHS = ['/tmp', '/run/user/{uid}', '/run/shm', '/dev/shm'] 9 | 10 | 11 | class MemoryTempfile: 12 | def __init__(self, preferred_paths: list = None, remove_paths: list or bool = None, 13 | additional_paths: list = None, filesystem_types: list = None, 14 | fallback: str or bool = None): 15 | self.os_tempdir = tempfile.gettempdir() 16 | suitable_paths = [self.os_tempdir] + SUITABLE_PATHS 17 | 18 | if isinstance(fallback, bool): 19 | self.fallback = self.os_tempdir if fallback else None 20 | else: 21 | self.fallback = fallback 22 | 23 | if platform.system() == "Linux": 24 | self.filesystem_types = list(filesystem_types) if filesystem_types is not None else MEM_BASED_FS 25 | 26 | preferred_paths = [] if preferred_paths is None else preferred_paths 27 | 28 | if isinstance(remove_paths, bool) and remove_paths: 29 | suitable_paths = [] 30 | elif isinstance(remove_paths, list) and len(remove_paths) > 0: 31 | suitable_paths = [i for i in suitable_paths if i not in remove_paths] 32 | 33 | additional_paths = [] if additional_paths is None else additional_paths 34 | 35 | self.suitable_paths = preferred_paths + suitable_paths + additional_paths 36 | 37 | uid = os.geteuid() 38 | 39 | with open('/proc/self/mountinfo', 'r') as file: 40 | mnt_info = {i[2]: i for i in [line.split() for line in file]} 41 | 42 | self.usable_paths = OrderedDict() 43 | for path in self.suitable_paths: 44 | path = path.replace('{uid}', str(uid)) 45 | 46 | # We may have repeated 47 | if self.usable_paths.get(path) is not None: 48 | continue 49 | self.usable_paths[path] = False 50 | try: 51 | dev = os.stat(path).st_dev 52 | major, minor = os.major(dev), os.minor(dev) 53 | mp = mnt_info.get("{}:{}".format(major, minor)) 54 | if mp and mp[mp.index("-",6)+1] in self.filesystem_types: 55 | self.usable_paths[path] = mp 56 | except FileNotFoundError: 57 | pass 58 | 59 | for key in [k for k, v in self.usable_paths.items() if not v]: 60 | del self.usable_paths[key] 61 | 62 | if len(self.usable_paths) > 0: 63 | self.tempdir = next(iter(self.usable_paths.keys())) 64 | else: 65 | if fallback: 66 | self.tempdir = self.fallback 67 | else: 68 | raise RuntimeError('No memory temporary dir found and fallback is disabled.') 69 | 70 | def found_mem_tempdir(self): 71 | return len(self.usable_paths) > 0 72 | 73 | def using_mem_tempdir(self): 74 | return self.tempdir in self.usable_paths 75 | 76 | def get_usable_mem_tempdir_paths(self): 77 | return list(self.usable_paths.keys()) 78 | 79 | def gettempdir(self): 80 | return self.tempdir 81 | 82 | def gettempdirb(self): 83 | return self.tempdir.encode(sys.getfilesystemencoding(), 'surrogateescape') 84 | 85 | def mkdtemp(self, suffix=None, prefix=None, dir=None): 86 | return tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=self.tempdir if not dir else dir) 87 | 88 | def mkstemp(self, suffix=None, prefix=None, dir=None, text=False): 89 | return tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=self.tempdir if not dir else dir, text=text) 90 | 91 | def TemporaryDirectory(self, suffix=None, prefix=None, dir=None): 92 | return tempfile.TemporaryDirectory(suffix=suffix, prefix=prefix, dir=self.tempdir if not dir else dir) 93 | 94 | def SpooledTemporaryFile(self, max_size=0, mode='w+b', buffering=-1, encoding=None, newline=None, 95 | suffix=None, prefix=None, dir=None): 96 | return tempfile.SpooledTemporaryFile(max_size=max_size, mode=mode, buffering=buffering, encoding=encoding, 97 | newline=newline, suffix=suffix, prefix=prefix, 98 | dir=self.tempdir if not dir else dir) 99 | 100 | def NamedTemporaryFile(self, mode='w+b', buffering=-1, encoding=None, newline=None, 101 | suffix=None, prefix=None, dir=None, delete=True): 102 | return tempfile.NamedTemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, 103 | suffix=suffix, prefix=prefix, dir=self.tempdir if not dir else dir, 104 | delete=delete) 105 | 106 | def TemporaryFile(self, mode='w+b', buffering=-1, encoding=None, newline=None, 107 | suffix=None, prefix=None, dir=None): 108 | return tempfile.TemporaryFile(mode=mode, buffering=buffering, encoding=encoding, newline=newline, 109 | suffix=suffix, prefix=prefix, dir=self.tempdir if not dir else dir) 110 | 111 | def gettempprefix(self): 112 | return tempfile.gettempdir() 113 | 114 | def gettempprefixb(self): 115 | return tempfile.gettempprefixb() 116 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | marker = "sys_platform == \"win32\"" 5 | name = "atomicwrites" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | version = "1.3.0" 9 | 10 | [[package]] 11 | category = "dev" 12 | description = "Classes Without Boilerplate" 13 | name = "attrs" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "19.3.0" 17 | 18 | [package.extras] 19 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 20 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 21 | docs = ["sphinx", "zope.interface"] 22 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 23 | 24 | [[package]] 25 | category = "dev" 26 | description = "Cross-platform colored terminal text." 27 | marker = "sys_platform == \"win32\"" 28 | name = "colorama" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 31 | version = "0.4.3" 32 | 33 | [[package]] 34 | category = "dev" 35 | description = "Read metadata from Python packages" 36 | marker = "python_version < \"3.8\"" 37 | name = "importlib-metadata" 38 | optional = false 39 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 40 | version = "1.4.0" 41 | 42 | [package.dependencies] 43 | zipp = ">=0.5" 44 | 45 | [package.extras] 46 | docs = ["sphinx", "rst.linker"] 47 | testing = ["packaging", "importlib-resources"] 48 | 49 | [[package]] 50 | category = "dev" 51 | description = "More routines for operating on iterables, beyond itertools" 52 | name = "more-itertools" 53 | optional = false 54 | python-versions = ">=3.5" 55 | version = "8.1.0" 56 | 57 | [[package]] 58 | category = "dev" 59 | description = "Core utilities for Python packages" 60 | name = "packaging" 61 | optional = false 62 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 63 | version = "20.0" 64 | 65 | [package.dependencies] 66 | pyparsing = ">=2.0.2" 67 | six = "*" 68 | 69 | [[package]] 70 | category = "dev" 71 | description = "plugin and hook calling mechanisms for python" 72 | name = "pluggy" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 75 | version = "0.13.1" 76 | 77 | [package.dependencies] 78 | [package.dependencies.importlib-metadata] 79 | python = "<3.8" 80 | version = ">=0.12" 81 | 82 | [package.extras] 83 | dev = ["pre-commit", "tox"] 84 | 85 | [[package]] 86 | category = "dev" 87 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 88 | name = "py" 89 | optional = false 90 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 91 | version = "1.8.1" 92 | 93 | [[package]] 94 | category = "dev" 95 | description = "Get CPU info with pure Python 2 & 3" 96 | name = "py-cpuinfo" 97 | optional = false 98 | python-versions = "*" 99 | version = "5.0.0" 100 | 101 | [[package]] 102 | category = "dev" 103 | description = "Python parsing module" 104 | name = "pyparsing" 105 | optional = false 106 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 107 | version = "2.4.6" 108 | 109 | [[package]] 110 | category = "dev" 111 | description = "pytest: simple powerful testing with Python" 112 | name = "pytest" 113 | optional = false 114 | python-versions = ">=3.5" 115 | version = "5.3.3" 116 | 117 | [package.dependencies] 118 | atomicwrites = ">=1.0" 119 | attrs = ">=17.4.0" 120 | colorama = "*" 121 | more-itertools = ">=4.0.0" 122 | packaging = "*" 123 | pluggy = ">=0.12,<1.0" 124 | py = ">=1.5.0" 125 | wcwidth = "*" 126 | 127 | [package.dependencies.importlib-metadata] 128 | python = "<3.8" 129 | version = ">=0.12" 130 | 131 | [package.extras] 132 | checkqa-mypy = ["mypy (v0.761)"] 133 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 134 | 135 | [[package]] 136 | category = "dev" 137 | description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. See calibration_ and FAQ_." 138 | name = "pytest-benchmark" 139 | optional = false 140 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 141 | version = "3.2.3" 142 | 143 | [package.dependencies] 144 | py-cpuinfo = "*" 145 | pytest = ">=3.8" 146 | 147 | [package.extras] 148 | aspect = ["aspectlib"] 149 | elasticsearch = ["elasticsearch"] 150 | histogram = ["pygal", "pygaljs"] 151 | 152 | [[package]] 153 | category = "dev" 154 | description = "Python 2 and 3 compatibility utilities" 155 | name = "six" 156 | optional = false 157 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 158 | version = "1.14.0" 159 | 160 | [[package]] 161 | category = "dev" 162 | description = "Measures number of Terminal column cells of wide-character codes" 163 | name = "wcwidth" 164 | optional = false 165 | python-versions = "*" 166 | version = "0.1.8" 167 | 168 | [[package]] 169 | category = "dev" 170 | description = "Backport of pathlib-compatible object wrapper for zip files" 171 | marker = "python_version < \"3.8\"" 172 | name = "zipp" 173 | optional = false 174 | python-versions = ">=3.6" 175 | version = "2.0.0" 176 | 177 | [package.dependencies] 178 | more-itertools = "*" 179 | 180 | [package.extras] 181 | docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] 182 | testing = ["pathlib2", "contextlib2", "unittest2"] 183 | 184 | [metadata] 185 | content-hash = "7471de4ba9345f90971c84769cbbfedebd646f06e4d4e31252db56bc74bb1926" 186 | python-versions = "^3.6" 187 | 188 | [metadata.files] 189 | atomicwrites = [ 190 | {file = "atomicwrites-1.3.0-py2.py3-none-any.whl", hash = "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4"}, 191 | {file = "atomicwrites-1.3.0.tar.gz", hash = "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"}, 192 | ] 193 | attrs = [ 194 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 195 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 196 | ] 197 | colorama = [ 198 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 199 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 200 | ] 201 | importlib-metadata = [ 202 | {file = "importlib_metadata-1.4.0-py2.py3-none-any.whl", hash = "sha256:bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359"}, 203 | {file = "importlib_metadata-1.4.0.tar.gz", hash = "sha256:f17c015735e1a88296994c0697ecea7e11db24290941983b08c9feb30921e6d8"}, 204 | ] 205 | more-itertools = [ 206 | {file = "more-itertools-8.1.0.tar.gz", hash = "sha256:c468adec578380b6281a114cb8a5db34eb1116277da92d7c46f904f0b52d3288"}, 207 | {file = "more_itertools-8.1.0-py3-none-any.whl", hash = "sha256:1a2a32c72400d365000412fe08eb4a24ebee89997c18d3d147544f70f5403b39"}, 208 | ] 209 | packaging = [ 210 | {file = "packaging-20.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"}, 211 | {file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"}, 212 | ] 213 | pluggy = [ 214 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 215 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 216 | ] 217 | py = [ 218 | {file = "py-1.8.1-py2.py3-none-any.whl", hash = "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"}, 219 | {file = "py-1.8.1.tar.gz", hash = "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa"}, 220 | ] 221 | py-cpuinfo = [ 222 | {file = "py-cpuinfo-5.0.0.tar.gz", hash = "sha256:2cf6426f776625b21d1db8397d3297ef7acfa59018f02a8779123f3190f18500"}, 223 | ] 224 | pyparsing = [ 225 | {file = "pyparsing-2.4.6-py2.py3-none-any.whl", hash = "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"}, 226 | {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, 227 | ] 228 | pytest = [ 229 | {file = "pytest-5.3.3-py3-none-any.whl", hash = "sha256:9f8d44f4722b3d06b41afaeb8d177cfbe0700f8351b1fc755dd27eedaa3eb9e0"}, 230 | {file = "pytest-5.3.3.tar.gz", hash = "sha256:f5d3d0e07333119fe7d4af4ce122362dc4053cdd34a71d2766290cf5369c64ad"}, 231 | ] 232 | pytest-benchmark = [ 233 | {file = "pytest-benchmark-3.2.3.tar.gz", hash = "sha256:ad4314d093a3089701b24c80a05121994c7765ce373478c8f4ba8d23c9ba9528"}, 234 | {file = "pytest_benchmark-3.2.3-py2.py3-none-any.whl", hash = "sha256:01f79d38d506f5a3a0a9ada22ded714537bbdfc8147a881a35c1655db07289d9"}, 235 | ] 236 | six = [ 237 | {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, 238 | {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, 239 | ] 240 | wcwidth = [ 241 | {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, 242 | {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, 243 | ] 244 | zipp = [ 245 | {file = "zipp-2.0.0-py3-none-any.whl", hash = "sha256:57147f6b0403b59f33fd357f169f860e031303415aeb7d04ede4839d23905ab8"}, 246 | {file = "zipp-2.0.0.tar.gz", hash = "sha256:7ae5ccaca427bafa9760ac3cd8f8c244bfc259794b5b6bb9db4dda2241575d09"}, 247 | ] 248 | --------------------------------------------------------------------------------