lfss 0.12.0__tar.gz → 0.12.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {lfss-0.12.0 → lfss-0.12.2}/PKG-INFO +10 -11
  2. {lfss-0.12.0 → lfss-0.12.2}/Readme.md +1 -1
  3. {lfss-0.12.0 → lfss-0.12.2}/docs/Client.md +1 -2
  4. {lfss-0.12.0 → lfss-0.12.2}/docs/changelog.md +8 -0
  5. {lfss-0.12.0 → lfss-0.12.2}/lfss/api/connector.py +2 -2
  6. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/__init__.py +4 -7
  7. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/cli.py +70 -22
  8. lfss-0.12.2/lfss/cli/cli_lib.py +64 -0
  9. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/utils.py +5 -1
  10. {lfss-0.12.0 → lfss-0.12.2}/pyproject.toml +11 -10
  11. {lfss-0.12.0 → lfss-0.12.2}/docs/Enviroment_variables.md +0 -0
  12. {lfss-0.12.0 → lfss-0.12.2}/docs/Known_issues.md +0 -0
  13. {lfss-0.12.0 → lfss-0.12.2}/docs/Permission.md +0 -0
  14. {lfss-0.12.0 → lfss-0.12.2}/docs/Webdav.md +0 -0
  15. {lfss-0.12.0 → lfss-0.12.2}/frontend/api.js +0 -0
  16. {lfss-0.12.0 → lfss-0.12.2}/frontend/index.html +0 -0
  17. {lfss-0.12.0 → lfss-0.12.2}/frontend/info.css +0 -0
  18. {lfss-0.12.0 → lfss-0.12.2}/frontend/info.js +0 -0
  19. {lfss-0.12.0 → lfss-0.12.2}/frontend/login.css +0 -0
  20. {lfss-0.12.0 → lfss-0.12.2}/frontend/login.js +0 -0
  21. {lfss-0.12.0 → lfss-0.12.2}/frontend/popup.css +0 -0
  22. {lfss-0.12.0 → lfss-0.12.2}/frontend/popup.js +0 -0
  23. {lfss-0.12.0 → lfss-0.12.2}/frontend/scripts.js +0 -0
  24. {lfss-0.12.0 → lfss-0.12.2}/frontend/state.js +0 -0
  25. {lfss-0.12.0 → lfss-0.12.2}/frontend/styles.css +0 -0
  26. {lfss-0.12.0 → lfss-0.12.2}/frontend/thumb.css +0 -0
  27. {lfss-0.12.0 → lfss-0.12.2}/frontend/thumb.js +0 -0
  28. {lfss-0.12.0 → lfss-0.12.2}/frontend/utils.js +0 -0
  29. {lfss-0.12.0 → lfss-0.12.2}/lfss/api/__init__.py +0 -0
  30. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/balance.py +0 -0
  31. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/log.py +0 -0
  32. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/panel.py +0 -0
  33. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/serve.py +0 -0
  34. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/user.py +0 -0
  35. {lfss-0.12.0 → lfss-0.12.2}/lfss/cli/vacuum.py +0 -0
  36. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/__init__.py +0 -0
  37. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/bounded_pool.py +0 -0
  38. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/config.py +0 -0
  39. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/connection_pool.py +0 -0
  40. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/database.py +0 -0
  41. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/datatype.py +0 -0
  42. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/error.py +0 -0
  43. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/log.py +0 -0
  44. {lfss-0.12.0 → lfss-0.12.2}/lfss/eng/thumb.py +0 -0
  45. {lfss-0.12.0 → lfss-0.12.2}/lfss/sql/init.sql +0 -0
  46. {lfss-0.12.0 → lfss-0.12.2}/lfss/sql/pragma.sql +0 -0
  47. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/app.py +0 -0
  48. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/app_base.py +0 -0
  49. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/app_dav.py +0 -0
  50. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/app_native.py +0 -0
  51. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/common_impl.py +0 -0
  52. {lfss-0.12.0 → lfss-0.12.2}/lfss/svc/request_log.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.12.0
3
+ Version: 0.12.2
4
4
  Summary: Lite file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: Li, Mengxun
@@ -10,16 +10,15 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
- Requires-Dist: aiofiles (==24.*)
14
- Requires-Dist: aiosqlite (==0.*)
15
- Requires-Dist: fastapi (==0.*)
16
- Requires-Dist: mimesniff (==1.*)
17
- Requires-Dist: pillow
18
- Requires-Dist: python-multipart
13
+ Provides-Extra: all
14
+ Requires-Dist: aiofiles (==24.*) ; extra == "all"
15
+ Requires-Dist: aiosqlite (==0.*) ; extra == "all"
16
+ Requires-Dist: fastapi[standard] (==0.*) ; extra == "all"
17
+ Requires-Dist: mimesniff (==1.*) ; extra == "all"
18
+ Requires-Dist: pillow ; extra == "all"
19
19
  Requires-Dist: requests (==2.*)
20
- Requires-Dist: rich
21
- Requires-Dist: stream-zip (==0.*)
22
- Requires-Dist: uvicorn (==0.*)
20
+ Requires-Dist: rich ; extra == "all"
21
+ Requires-Dist: stream-zip (==0.*) ; extra == "all"
23
22
  Project-URL: Repository, https://github.com/MenxLi/lfss
24
23
  Description-Content-Type: text/markdown
25
24
 
@@ -41,7 +40,7 @@ Tested on 2 million files, and it is still fast.
41
40
 
42
41
  Usage:
43
42
  ```sh
44
- pip install lfss
43
+ pip install "lfss[all]"
45
44
  lfss-user add <username> <password>
46
45
  lfss-serve
47
46
  ```
@@ -16,7 +16,7 @@ Tested on 2 million files, and it is still fast.
16
16
 
17
17
  Usage:
18
18
  ```sh
19
- pip install lfss
19
+ pip install "lfss[all]"
20
20
  lfss-user add <username> <password>
21
21
  lfss-serve
22
22
  ```
@@ -3,8 +3,7 @@
3
3
 
4
4
  To install python CLI tools without dependencies (to avoid conflicts with your existing packages):
5
5
  ```sh
6
- pip install requests
7
- pip install lfss --no-deps
6
+ pip install lfss
8
7
  ```
9
8
 
10
9
  Then set the `LFSS_ENDPOINT`, `LFSS_TOKEN` environment variables,
@@ -1,5 +1,13 @@
1
1
  ## 0.12
2
2
 
3
+ ### 0.12.2
4
+ - Setup optional dependencies
5
+ - Present only the name by default for CLI list command
6
+
7
+ ### 0.12.1
8
+ - Add `cat` command
9
+ - Use unicode icons for CLI list command
10
+
3
11
  ### 0.12.0
4
12
  - Change default script to client CLI
5
13
  - Client CLI default to verbose output
@@ -212,12 +212,12 @@ class Connector:
212
212
  if response is None: return None
213
213
  return response.content
214
214
 
215
- def get_stream(self, path: str) -> Iterator[bytes]:
215
+ def get_stream(self, path: str, chunk_size = 1024) -> Iterator[bytes]:
216
216
  """Downloads a file from the specified path, will raise PathNotFoundError if path not found."""
217
217
  path = _p(path)
218
218
  response = self._get(path, stream=True)
219
219
  if response is None: raise PathNotFoundError("Path not found: " + path)
220
- return response.iter_content(chunk_size=1024)
220
+ return response.iter_content(chunk_size)
221
221
 
222
222
  def get_json(self, path: str) -> Optional[dict]:
223
223
  path = _p(path)
@@ -16,16 +16,13 @@ def catch_request_error(error_code_handler: Optional[ dict[int, Callable[[reques
16
16
  print(f"\033[91m[Error message]: {e.response.text}\033[0m")
17
17
 
18
18
  T = TypeVar('T')
19
- def line_sep(iter: Iterable[T], enable=True, start=True, end=True, color="\033[90m") -> Generator[T, None, None]:
19
+ def line_sep(iter: Iterable[T], enable=True, start=True, end=True, middle=False, color="\033[90m") -> Generator[T, None, None]:
20
20
  screen_width = os.get_terminal_size().columns
21
21
  def print_ln():
22
22
  if enable: print(color + "-" * screen_width + "\033[0m")
23
23
 
24
- if start:
25
- print_ln()
24
+ if start: print_ln()
26
25
  for i, line in enumerate(iter):
27
- if i > 0:
28
- print_ln()
26
+ if i > 0 and middle: print_ln()
29
27
  yield line
30
- if end:
31
- print_ln()
28
+ if end: print_ln()
@@ -1,12 +1,15 @@
1
1
  from pathlib import Path
2
2
  import argparse, typing, sys
3
3
  from lfss.api import Connector, upload_directory, upload_file, download_file, download_directory
4
- from lfss.eng.datatype import FileReadPermission, FileSortKey, DirSortKey, AccessLevel
4
+ from lfss.eng.datatype import (
5
+ FileReadPermission, AccessLevel,
6
+ FileSortKey, DirSortKey,
7
+ FileRecord, DirectoryRecord, PathContents
8
+ )
5
9
  from lfss.eng.utils import decode_uri_components, fmt_storage_size
6
- from . import catch_request_error, line_sep as _line_sep
7
10
 
8
- # monkey patch to avoid printing line separators...may remove line_sep in the future
9
- line_sep = lambda *args, **kwargs: _line_sep(*args, enable=False, **kwargs)
11
+ from . import catch_request_error, line_sep
12
+ from .cli_lib import mimetype_unicode, stream_text
10
13
 
11
14
  def parse_permission(s: str) -> FileReadPermission:
12
15
  for p in FileReadPermission:
@@ -25,6 +28,50 @@ def default_error_handler_dict(path: str):
25
28
  404: lambda _: print(f"\033[31mNot found\033[0m ({path})", file=sys.stderr),
26
29
  409: lambda _: print(f"\033[31mConflict\033[0m ({path})", file=sys.stderr),
27
30
  }
31
+ def print_path_list(
32
+ path_list: list[FileRecord] | list[DirectoryRecord] | PathContents,
33
+ detailed: bool = False
34
+ ):
35
+ dirs: list[DirectoryRecord]
36
+ files: list[FileRecord]
37
+ if isinstance(path_list, PathContents):
38
+ dirs = path_list.dirs
39
+ files = path_list.files
40
+ else:
41
+ dirs = [p for p in path_list if isinstance(p, DirectoryRecord)]
42
+ files = [p for p in path_list if isinstance(p, FileRecord)]
43
+ # check if terminal supports unicode
44
+ supports_unicode = sys.stdout.encoding.lower().startswith("utf")
45
+ def print_ln(r: DirectoryRecord | FileRecord):
46
+ nonlocal detailed, supports_unicode
47
+ match (r, supports_unicode):
48
+ case (DirectoryRecord(), True):
49
+ print(mimetype_unicode(r), end=" ")
50
+ case (DirectoryRecord(), False):
51
+ print("[D]", end=" ")
52
+ case (FileRecord(), True):
53
+ print(mimetype_unicode(r), end=" ")
54
+ case (FileRecord(), False):
55
+ print("[F]", end=" ")
56
+ case _:
57
+ print("[?]", end=" ")
58
+ # if not detailed, only print name
59
+ if not detailed:
60
+ if isinstance(r, DirectoryRecord):
61
+ assert r.url.endswith("/")
62
+ print(decode_uri_components(r.url).rstrip("/").split("/")[-1], end="/")
63
+ else:
64
+ print(decode_uri_components(r.url).split("/")[-1], end="")
65
+ else:
66
+ print(decode_uri_components(r.url), end="")
67
+ if isinstance(r, FileRecord):
68
+ print(f" :: {fmt_storage_size(r.file_size)} {r.permission.name}", end="")
69
+ print()
70
+
71
+ for d in line_sep(dirs, end=False):
72
+ print_ln(d)
73
+ for f in line_sep(files, start=False):
74
+ print_ln(f)
28
75
 
29
76
  def parse_arguments():
30
77
  parser = argparse.ArgumentParser(description="Client-side command line interface, set LFSS_ENDPOINT and LFSS_TOKEN environment variables for authentication.")
@@ -74,7 +121,7 @@ def parse_arguments():
74
121
  sp_list.add_argument("path", help="Path to list", type=str)
75
122
  sp_list.add_argument("--offset", type=int, default=0, help="Offset of the list")
76
123
  sp_list.add_argument("--limit", type=int, default=100, help="Limit of the list")
77
- sp_list.add_argument("-l", "--long", action="store_true", help="Detailed list, including all metadata")
124
+ sp_list.add_argument("-l", "--long", action="store_true", help="Detailed list")
78
125
  sp_list.add_argument("--order", "--order-by", type=str, help="Order of the list", default="", choices=typing.get_args(FileSortKey))
79
126
  sp_list.add_argument("--reverse", "--order-desc", action="store_true", help="Reverse the list order")
80
127
 
@@ -83,7 +130,7 @@ def parse_arguments():
83
130
  sp_list_d.add_argument("path", help="Path to list", type=str)
84
131
  sp_list_d.add_argument("--offset", type=int, default=0, help="Offset of the list")
85
132
  sp_list_d.add_argument("--limit", type=int, default=100, help="Limit of the list")
86
- sp_list_d.add_argument("-l", "--long", action="store_true", help="Detailed list, including all metadata")
133
+ sp_list_d.add_argument("-l", "--long", action="store_true", help="Detailed list")
87
134
  sp_list_d.add_argument("--order", "--order-by", type=str, help="Order of the list", default="", choices=typing.get_args(DirSortKey))
88
135
  sp_list_d.add_argument("--reverse", "--order-desc", action="store_true", help="Reverse the list order")
89
136
 
@@ -93,10 +140,14 @@ def parse_arguments():
93
140
  sp_list_f.add_argument("--offset", type=int, default=0, help="Offset of the list")
94
141
  sp_list_f.add_argument("--limit", type=int, default=100, help="Limit of the list")
95
142
  sp_list_f.add_argument("-r", "--recursive", "--flat", action="store_true", help="List files recursively")
96
- sp_list_f.add_argument("-l", "--long", action="store_true", help="Detailed list, including all metadata")
143
+ sp_list_f.add_argument("-l", "--long", action="store_true", help="Detailed list")
97
144
  sp_list_f.add_argument("--order", "--order-by", type=str, help="Order of the list", default="", choices=typing.get_args(FileSortKey))
98
145
  sp_list_f.add_argument("--reverse", "--order-desc", action="store_true", help="Reverse the list order")
99
146
 
147
+ # show content
148
+ sp_show = sp.add_parser("concatenate", help="Concatenate and print files", aliases=["cat"])
149
+ sp_show.add_argument("path", help="Path to the text files", type=str, nargs="+")
150
+ sp_show.add_argument("-e", "--encoding", type=str, default="utf-8", help="Text file encoding, default utf-8")
100
151
  return parser.parse_args()
101
152
 
102
153
  def main():
@@ -213,13 +264,7 @@ def main():
213
264
  order_by=args.order,
214
265
  order_desc=args.reverse,
215
266
  )
216
- for i, d in enumerate(line_sep(res.dirs, end=False)):
217
- d.url = decode_uri_components(d.url)
218
- print(f"[d{i+1}] {d if args.long else d.url}")
219
- for i, f in enumerate(line_sep(res.files)):
220
- f.url = decode_uri_components(f.url)
221
- print(f"[f{i+1}] {f if args.long else f.url}")
222
-
267
+ print_path_list(res, detailed=args.long)
223
268
  if len(res.dirs) + len(res.files) == args.limit:
224
269
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more items.\033[0m")
225
270
 
@@ -233,10 +278,7 @@ def main():
233
278
  order_by=args.order,
234
279
  order_desc=args.reverse,
235
280
  )
236
- for i, f in enumerate(line_sep(res)):
237
- f.url = decode_uri_components(f.url)
238
- print(f"[{i+1}] {f if args.long else f.url}")
239
-
281
+ print_path_list(res, detailed=args.long)
240
282
  if len(res) == args.limit:
241
283
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more files.\033[0m")
242
284
 
@@ -250,13 +292,19 @@ def main():
250
292
  order_by=args.order,
251
293
  order_desc=args.reverse,
252
294
  )
253
- for i, d in enumerate(line_sep(res)):
254
- d.url = decode_uri_components(d.url)
255
- print(f"[{i+1}] {d if args.long else d.url}")
256
-
295
+ print_path_list(res, detailed=args.long)
257
296
  if len(res) == args.limit:
258
297
  print(f"\033[33m[Warning] List limit reached, use --offset and --limit to list more directories.\033[0m")
259
298
 
299
+ elif args.command in ["cat", "concatenate"]:
300
+ for _p in args.path:
301
+ with catch_request_error(default_error_handler_dict(_p)):
302
+ try:
303
+ for chunk in stream_text(connector, _p, encoding=args.encoding):
304
+ print(chunk, end="")
305
+ except (FileNotFoundError, ValueError) as e:
306
+ print(f"\033[31m{e}\033[0m", file=sys.stderr)
307
+
260
308
  else:
261
309
  raise NotImplementedError(f"Command {args.command} not implemented.")
262
310
 
@@ -0,0 +1,64 @@
1
+
2
+ from ..api.connector import Connector
3
+ from ..eng.datatype import DirectoryRecord, FileRecord
4
+
5
+ def mimetype_unicode(r: DirectoryRecord | FileRecord):
6
+ if isinstance(r, DirectoryRecord):
7
+ return "📁"
8
+ if r.mime_type in ["application/pdf", "application/x-pdf"]:
9
+ return "📕"
10
+ elif r.mime_type.startswith("image/"):
11
+ return "🖼️"
12
+ elif r.mime_type.startswith("video/"):
13
+ return "🎞️"
14
+ elif r.mime_type.startswith("audio/"):
15
+ return "🎵"
16
+ elif r.mime_type in ["application/zip", "application/x-tar", "application/gzip", "application/x-7z-compressed"]:
17
+ return "📦"
18
+ elif r.mime_type in ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]:
19
+ return "📊"
20
+ elif r.mime_type in ["application/x-msdownload", "application/x-executable", "application/x-mach-binary", "application/x-elf"]:
21
+ return "💻"
22
+ elif r.mime_type in ["application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation"]:
23
+ return "📈"
24
+ elif r.mime_type in set([
25
+ "text/html", "application/xhtml+xml", "application/xml", "text/css", "text/x-scss", "application/javascript", "text/javascript",
26
+ "application/json", "text/x-yaml", "text/x-markdown", "application/wasm",
27
+ "text/x-ruby", "application/x-ruby", "text/x-perl", "application/x-lisp",
28
+ "text/x-haskell", "text/x-lua", "application/x-tcl",
29
+ "text/x-python", "text/x-java-source", "text/x-go", "application/x-rust", "text/x-asm",
30
+ "application/sql", "text/x-c", "text/x-c++", "text/x-csharp",
31
+ "application/x-httpd-php", "application/x-sh", "application/x-shellscript",
32
+ "application/x-latex", "application/x-tex",
33
+ ]):
34
+ return "👨‍💻"
35
+ elif r.mime_type.startswith("text/"):
36
+ return "📃"
37
+ return "📄"
38
+
39
+ def stream_text(
40
+ conn: Connector,
41
+ path: str,
42
+ encoding="utf-8",
43
+ chunk_size=1024 * 8,
44
+ ):
45
+ """
46
+ Stream text content of a file from the server.
47
+ Raise FileNotFoundError if the file does not exist.
48
+ Raise ValueError if the file size exceeds MAX_TEXT_SIZE.
49
+
50
+ Yields str chunks.
51
+ """
52
+ MAX_TEXT_SIZE = 100 * 1024 * 1024 # 100 MB
53
+ r = conn.get_fmeta(path)
54
+ if r is None:
55
+ raise FileNotFoundError(f"File not found: {path}")
56
+ if r.file_size > MAX_TEXT_SIZE:
57
+ raise ValueError(f"File size {r.file_size} exceeds maximum text size {MAX_TEXT_SIZE}")
58
+ ss = conn.get_stream(r.url, chunk_size=chunk_size)
59
+ total_read = 0
60
+ for chunk in ss:
61
+ total_read += len(chunk)
62
+ if total_read > MAX_TEXT_SIZE:
63
+ raise ValueError(f"File size exceeds maximum text size {MAX_TEXT_SIZE}")
64
+ yield chunk.decode(encoding, errors='replace') # decode bytes to str, replace errors
@@ -3,7 +3,6 @@ import urllib.parse
3
3
  import pathlib
4
4
  import functools
5
5
  import hashlib
6
- import aiofiles
7
6
  import asyncio
8
7
  from asyncio import Lock
9
8
  from collections import OrderedDict
@@ -11,6 +10,11 @@ from concurrent.futures import ThreadPoolExecutor
11
10
  from typing import TypeVar, Callable, Awaitable
12
11
  from functools import wraps, partial
13
12
  from uuid import uuid4
13
+ try:
14
+ # optional dependency for client-side
15
+ import aiofiles
16
+ except ImportError:
17
+ pass
14
18
 
15
19
  async def copy_file(source: str|pathlib.Path, destination: str|pathlib.Path):
16
20
  async with aiofiles.open(source, mode='rb') as src:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "lfss"
3
- version = "0.12.0"
3
+ version = "0.12.2"
4
4
  description = "Lite file storage service"
5
5
  authors = ["Li, Mengxun <mengxunli@whu.edu.cn>"]
6
6
  readme = "Readme.md"
@@ -11,15 +11,16 @@ include = ["Readme.md", "docs/*", "frontend/*", "lfss/sql/*"]
11
11
  [tool.poetry.dependencies]
12
12
  python = ">=3.10" # PEP-622
13
13
  requests = "2.*"
14
- aiosqlite = "0.*"
15
- aiofiles = "24.*"
16
- mimesniff = "1.*"
17
- fastapi = "0.*"
18
- uvicorn = "0.*"
19
- stream-zip = "0.*"
20
- python-multipart = "*"
21
- pillow = "*"
22
- rich = "*"
14
+ aiosqlite = {"version" = "0.*", "optional" = true}
15
+ aiofiles = {"version" = "24.*", "optional" = true}
16
+ mimesniff = {"version" = "1.*", "optional" = true}
17
+ stream-zip = {"version" = "0.*", "optional" = true}
18
+ pillow = {"version" = "*", "optional" = true}
19
+ rich = {"version" = "*", "optional" = true}
20
+ fastapi = {"version" = "0.*", "optional" = true, "extras" = ["standard"]}
21
+
22
+ [tool.poetry.extras]
23
+ all = ["aiosqlite", "aiofiles", "mimesniff", "fastapi", "stream-zip", "pillow", "rich"]
23
24
 
24
25
  [tool.poetry.dev-dependencies]
25
26
  pytest = "*"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes