lfss 0.9.5__py3-none-any.whl → 0.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Readme.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Lite File Storage Service (LFSS)
2
- [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/)
2
+ [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/lfss)](https://pypi.org/project/lfss/)
3
3
 
4
4
  My experiment on a lightweight and high-performance file/object storage service...
5
5
 
docs/Permission.md CHANGED
@@ -22,16 +22,16 @@ A file is owned by the user who created it, may not necessarily be the user unde
22
22
  ## File access with `GET` permission
23
23
 
24
24
  ### File access
25
- For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the owner and the file.
25
+ For accessing file content, the user must have `GET` permission of the file, which is determined by the `permission` field of both the path-owner and the file.
26
26
 
27
27
  There are four types of permissions: `unset`, `public`, `protected`, `private`.
28
28
  Non-admin users can access files based on:
29
29
 
30
30
  - If the file is `public`, then all users can access it.
31
31
  - If the file is `protected`, then only the logged-in user can access it.
32
- - If the file is `private`, then only the owner can access it.
33
- - If the file is `unset`, then the file's permission is inherited from the owner's permission.
34
- - If both the owner and the file have `unset` permission, then the file is `public`.
32
+ - If the file is `private`, then only the owner/path-owner can access it.
33
+ - If the file is `unset`, then the file's permission is inherited from the path-owner's permission.
34
+ - If both the path-owner and the file have `unset` permission, then the file is `public`.
35
35
 
36
36
  ## File creation with `PUT`/`POST` permission
37
37
  `PUT`/`POST` permission is not allowed for non-peer users.
lfss/api/connector.py CHANGED
@@ -15,12 +15,13 @@ from lfss.eng.utils import ensure_uri_compnents
15
15
 
16
16
  _default_endpoint = os.environ.get('LFSS_ENDPOINT', 'http://localhost:8000')
17
17
  _default_token = os.environ.get('LFSS_TOKEN', '')
18
+ num_t = float | int
18
19
 
19
20
  class Connector:
20
21
  class Session:
21
22
  def __init__(
22
23
  self, connector: Connector, pool_size: int = 10,
23
- retry: int = 1, backoff_factor: float = 0.5, status_forcelist: list[int] = [503]
24
+ retry: int = 1, backoff_factor: num_t = 0.5, status_forcelist: list[int] = [503]
24
25
  ):
25
26
  self.connector = connector
26
27
  self.pool_size = pool_size
@@ -47,13 +48,21 @@ class Connector:
47
48
  def __exit__(self, exc_type, exc_value, traceback):
48
49
  self.close()
49
50
 
50
- def __init__(self, endpoint=_default_endpoint, token=_default_token):
51
+ def __init__(self, endpoint=_default_endpoint, token=_default_token, timeout: Optional[num_t | tuple[num_t, num_t]]=None, verify: Optional[bool | str] = None):
52
+ """
53
+ - endpoint: the URL of the LFSS server. Default to $LFSS_ENDPOINT or http://localhost:8000.
54
+ - token: the access token. Default to $LFSS_TOKEN.
55
+ - timeout: the timeout for each request, can be either a single value or a tuple of two values (connect, read), refer to requests.Session.request.
56
+ - verify: either a boolean or a string, to control SSL verification. Default to True, refer to requests.Session.request.
57
+ """
51
58
  assert token, "No token provided. Please set LFSS_TOKEN environment variable."
52
59
  self.config = {
53
60
  "endpoint": endpoint,
54
61
  "token": token
55
62
  }
56
63
  self._session: Optional[requests.Session] = None
64
+ self.timeout = timeout
65
+ self.verify = verify
57
66
 
58
67
  def session( self, pool_size: int = 10, **kwargs):
59
68
  """ avoid creating a new session for each request. """
@@ -74,11 +83,11 @@ class Connector:
74
83
  })
75
84
  headers.update(extra_headers)
76
85
  if self._session is not None:
77
- response = self._session.request(method, url, headers=headers, **kwargs)
86
+ response = self._session.request(method, url, headers=headers, timeout=self.timeout, verify=self.verify, **kwargs)
78
87
  response.raise_for_status()
79
88
  else:
80
89
  with requests.Session() as s:
81
- response = s.request(method, url, headers=headers, **kwargs)
90
+ response = s.request(method, url, headers=headers, timeout=self.timeout, verify=self.verify, **kwargs)
82
91
  response.raise_for_status()
83
92
  return response
84
93
  return f
@@ -8,7 +8,7 @@ from functools import wraps
8
8
  from typing import Callable, Awaitable
9
9
 
10
10
  from .log import get_logger
11
- from .error import DatabaseLockedError
11
+ from .error import DatabaseLockedError, DatabaseTransactionError
12
12
  from .config import DATA_HOME
13
13
 
14
14
  async def execute_sql(conn: aiosqlite.Connection | aiosqlite.Cursor, name: str):
@@ -147,6 +147,14 @@ def global_entrance(n_read: int = 1):
147
147
  return wrapper
148
148
  return decorator
149
149
 
150
+ def handle_sqlite_error(e: Exception):
151
+ if 'database is locked' in str(e):
152
+ raise DatabaseLockedError from e
153
+ if 'cannot start a transaction within a transaction' in str(e):
154
+ get_logger('database', global_instance=True).error(f"Unexpected error: {e}")
155
+ raise DatabaseTransactionError from e
156
+ raise e
157
+
150
158
  @asynccontextmanager
151
159
  async def unique_cursor(is_write: bool = False):
152
160
  if not is_write:
@@ -155,9 +163,7 @@ async def unique_cursor(is_write: bool = False):
155
163
  try:
156
164
  yield await connection_obj.conn.cursor()
157
165
  except Exception as e:
158
- if 'database is locked' in str(e):
159
- raise DatabaseLockedError from e
160
- raise e
166
+ handle_sqlite_error(e)
161
167
  finally:
162
168
  await g_pool.release(connection_obj)
163
169
  else:
@@ -166,9 +172,7 @@ async def unique_cursor(is_write: bool = False):
166
172
  try:
167
173
  yield await connection_obj.conn.cursor()
168
174
  except Exception as e:
169
- if 'database is locked' in str(e):
170
- raise DatabaseLockedError from e
171
- raise e
175
+ handle_sqlite_error(e)
172
176
  finally:
173
177
  await g_pool.release(connection_obj)
174
178
 
lfss/eng/database.py CHANGED
@@ -965,6 +965,11 @@ class Database:
965
965
  async def zip_path(self, top_url: str, op_user: Optional[UserRecord]) -> io.BytesIO:
966
966
  if top_url.startswith('/'):
967
967
  top_url = top_url[1:]
968
+
969
+ if op_user:
970
+ if await check_path_permission(top_url, op_user) < AccessLevel.READ:
971
+ raise PermissionDeniedError(f"Permission denied: {op_user.username} cannot zip path {top_url}")
972
+
968
973
  buffer = io.BytesIO()
969
974
  with zipfile.ZipFile(buffer, 'w') as zf:
970
975
  async for (r, blob) in self.iter_path(top_url, None):
@@ -979,39 +984,50 @@ class Database:
979
984
  buffer.seek(0)
980
985
  return buffer
981
986
 
982
- def check_file_read_permission(user: UserRecord, owner: UserRecord, file: FileRecord) -> tuple[bool, str]:
987
+ async def _get_path_owner(cur: aiosqlite.Cursor, path: str) -> UserRecord:
988
+ path_username = path.split('/')[0]
989
+ uconn = UserConn(cur)
990
+ path_user = await uconn.get_user(path_username)
991
+ if path_user is None:
992
+ raise PathNotFoundError(f"Invalid path: {path_username} is not a valid username")
993
+ return path_user
994
+
995
+ async def check_file_read_permission(user: UserRecord, file: FileRecord, cursor: Optional[aiosqlite.Cursor] = None) -> tuple[bool, str]:
983
996
  """
984
997
  This does not consider alias level permission,
985
998
  use check_path_permission for alias level permission check first:
986
999
  ```
987
1000
  if await check_path_permission(path, user) < AccessLevel.READ:
988
- read_allowed, reason = check_file_read_permission(user, owner, file)
1001
+ read_allowed, reason = check_file_read_permission(user, file)
989
1002
  ```
1003
+ The implementation assumes the user is not admin and is not the owner of the file/path
990
1004
  """
991
- if user.is_admin:
992
- return True, ""
1005
+ @asynccontextmanager
1006
+ async def this_cur():
1007
+ if cursor is None:
1008
+ async with unique_cursor() as _cur:
1009
+ yield _cur
1010
+ else:
1011
+ yield cursor
1012
+
1013
+ f_perm = file.permission
1014
+
1015
+ # if file permission unset, use path owner's permission as fallback
1016
+ if f_perm == FileReadPermission.UNSET:
1017
+ async with this_cur() as cur:
1018
+ path_owner = await _get_path_owner(cur, file.url)
1019
+ f_perm = path_owner.permission
993
1020
 
994
1021
  # check permission of the file
995
- if file.permission == FileReadPermission.PRIVATE:
996
- if user.id != owner.id:
997
- return False, "Permission denied, private file"
998
- elif file.permission == FileReadPermission.PROTECTED:
1022
+ if f_perm == FileReadPermission.PRIVATE:
1023
+ return False, "Permission denied, private file"
1024
+ elif f_perm == FileReadPermission.PROTECTED:
999
1025
  if user.id == 0:
1000
1026
  return False, "Permission denied, protected file"
1001
- elif file.permission == FileReadPermission.PUBLIC:
1027
+ elif f_perm == FileReadPermission.PUBLIC:
1002
1028
  return True, ""
1003
1029
  else:
1004
- assert file.permission == FileReadPermission.UNSET
1005
-
1006
- # use owner's permission as fallback
1007
- if owner.permission == FileReadPermission.PRIVATE:
1008
- if user.id != owner.id:
1009
- return False, "Permission denied, private user file"
1010
- elif owner.permission == FileReadPermission.PROTECTED:
1011
- if user.id == 0:
1012
- return False, "Permission denied, protected user file"
1013
- else:
1014
- assert owner.permission == FileReadPermission.PUBLIC or owner.permission == FileReadPermission.UNSET
1030
+ assert f_perm == FileReadPermission.UNSET
1015
1031
 
1016
1032
  return True, ""
1017
1033
 
@@ -1034,15 +1050,11 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
1034
1050
  yield cursor
1035
1051
 
1036
1052
  # check if path user exists
1037
- path_username = path.split('/')[0]
1038
1053
  async with this_cur() as cur:
1039
- uconn = UserConn(cur)
1040
- path_user = await uconn.get_user(path_username)
1041
- if path_user is None:
1042
- raise PathNotFoundError(f"Invalid path: {path_username} is not a valid username")
1054
+ path_owner = await _get_path_owner(cur, path)
1043
1055
 
1044
- # check if user is admin
1045
- if user.is_admin or user.username == path_username:
1056
+ # check if user is admin or the owner of the path
1057
+ if user.is_admin or user.id == path_owner.id:
1046
1058
  return AccessLevel.ALL
1047
1059
 
1048
1060
  # if the path is a file, check if the user is the owner
@@ -1056,4 +1068,4 @@ async def check_path_permission(path: str, user: UserRecord, cursor: Optional[ai
1056
1068
  # check alias level
1057
1069
  async with this_cur() as cur:
1058
1070
  uconn = UserConn(cur)
1059
- return await uconn.query_peer_level(user.id, path_user.id)
1071
+ return await uconn.query_peer_level(user.id, path_owner.id)
lfss/eng/error.py CHANGED
@@ -12,6 +12,8 @@ class InvalidPathError(LFSSExceptionBase, ValueError):...
12
12
 
13
13
  class DatabaseLockedError(LFSSExceptionBase, sqlite3.DatabaseError):...
14
14
 
15
+ class DatabaseTransactionError(LFSSExceptionBase, sqlite3.DatabaseError):...
16
+
15
17
  class PathNotFoundError(LFSSExceptionBase, FileNotFoundError):...
16
18
 
17
19
  class FileDuplicateError(LFSSExceptionBase, FileExistsError):...
lfss/svc/app_base.py CHANGED
@@ -54,6 +54,7 @@ def handle_exception(fn):
54
54
  if isinstance(e, FileExistsError): raise HTTPException(status_code=409, detail=str(e))
55
55
  if isinstance(e, TooManyItemsError): raise HTTPException(status_code=400, detail=str(e))
56
56
  if isinstance(e, DatabaseLockedError): raise HTTPException(status_code=503, detail=str(e))
57
+ if isinstance(e, DatabaseTransactionError): raise HTTPException(status_code=503, detail=str(e))
57
58
  if isinstance(e, FileLockedError): raise HTTPException(status_code=423, detail=str(e))
58
59
  logger.error(f"Uncaptured error in {fn.__name__}: {e}")
59
60
  raise
lfss/svc/app_native.py CHANGED
@@ -5,6 +5,7 @@ from fastapi.responses import StreamingResponse
5
5
  from fastapi.exceptions import HTTPException
6
6
 
7
7
  from ..eng.utils import ensure_uri_compnents
8
+ from ..eng.config import MAX_MEM_FILE_BYTES
8
9
  from ..eng.connection_pool import unique_cursor
9
10
  from ..eng.database import check_file_read_permission, check_path_permission, UserConn, FileConn
10
11
  from ..eng.datatype import (
@@ -92,14 +93,29 @@ async def bundle_files(path: str, user: UserRecord = Depends(registered_user)):
92
93
  dir_record = await FileConn(cur).get_path_record(path)
93
94
 
94
95
  pathname = f"{path.split('/')[-2]}"
95
- return StreamingResponse(
96
- content = await db.zip_path_stream(path, op_user=user),
97
- media_type = "application/zip",
98
- headers = {
99
- f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
100
- "X-Content-Bytes": str(dir_record.size),
101
- }
102
- )
96
+
97
+ if dir_record.size < MAX_MEM_FILE_BYTES:
98
+ logger.debug(f"Bundle {path} in memory")
99
+ dir_bytes = (await db.zip_path(path, op_user=user)).getvalue()
100
+ return Response(
101
+ content = dir_bytes,
102
+ media_type = "application/zip",
103
+ headers = {
104
+ f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
105
+ "Content-Length": str(len(dir_bytes)),
106
+ "X-Content-Bytes": str(dir_record.size),
107
+ }
108
+ )
109
+ else:
110
+ logger.debug(f"Bundle {path} in stream")
111
+ return StreamingResponse(
112
+ content = await db.zip_path_stream(path, op_user=user),
113
+ media_type = "application/zip",
114
+ headers = {
115
+ f"Content-Disposition": f"attachment; filename=bundle-{pathname}.zip",
116
+ "X-Content-Bytes": str(dir_record.size),
117
+ }
118
+ )
103
119
 
104
120
  @router_api.get("/meta")
105
121
  @handle_exception
@@ -112,9 +128,7 @@ async def get_file_meta(path: str, user: UserRecord = Depends(registered_user)):
112
128
  if is_file:
113
129
  record = await fconn.get_file_record(path, throw=True)
114
130
  if await check_path_permission(path, user, cursor=cur) < AccessLevel.READ:
115
- uconn = UserConn(cur)
116
- owner = await uconn.get_user_by_id(record.owner_id, throw=True)
117
- is_allowed, reason = check_file_read_permission(user, owner, record)
131
+ is_allowed, reason = await check_file_read_permission(user, record, cursor=cur)
118
132
  if not is_allowed:
119
133
  raise HTTPException(status_code=403, detail=reason)
120
134
  else:
lfss/svc/common_impl.py CHANGED
@@ -112,11 +112,8 @@ async def get_impl(
112
112
  async with unique_cursor() as cur:
113
113
  fconn = FileConn(cur)
114
114
  file_record = await fconn.get_file_record(path, throw=True)
115
- uconn = UserConn(cur)
116
- owner = await uconn.get_user_by_id(file_record.owner_id, throw=True)
117
-
118
115
  if not await check_path_permission(path, user, cursor=cur) >= AccessLevel.READ:
119
- allow_access, reason = check_file_read_permission(user, owner, file_record)
116
+ allow_access, reason = await check_file_read_permission(user, file_record, cursor=cur)
120
117
  if not allow_access:
121
118
  raise HTTPException(status_code=403 if user.id != 0 else 401, detail=reason)
122
119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lfss
3
- Version: 0.9.5
3
+ Version: 0.10.0
4
4
  Summary: Lightweight file storage service
5
5
  Home-page: https://github.com/MenxLi/lfss
6
6
  Author: li_mengxun
@@ -23,7 +23,7 @@ Project-URL: Repository, https://github.com/MenxLi/lfss
23
23
  Description-Content-Type: text/markdown
24
24
 
25
25
  # Lite File Storage Service (LFSS)
26
- [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/)
26
+ [![PyPI](https://img.shields.io/pypi/v/lfss)](https://pypi.org/project/lfss/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/lfss)](https://pypi.org/project/lfss/)
27
27
 
28
28
  My experiment on a lightweight and high-performance file/object storage service...
29
29
 
@@ -1,8 +1,8 @@
1
- Readme.md,sha256=ST2E12DJVlKTReiJjRBc7KZyAr8KyqlcK2BoTc_Peaw,1829
1
+ Readme.md,sha256=B-foESzFWoSI5MEd89AWUzKcVRrTwipM28TK8GN0o8c,1920
2
2
  docs/Changelog.md,sha256=QYej_hmGnv9t8wjFHXBvmrBOvY7aACZ82oa5SVkIyzM,882
3
3
  docs/Enviroment_variables.md,sha256=xaL8qBwT8B2Qe11FaOU3xWrRCh1mJ1VyTFCeFbkd0rs,570
4
4
  docs/Known_issues.md,sha256=ZqETcWP8lzTOel9b2mxEgCnADFF8IxOrEtiVO1NoMAk,251
5
- docs/Permission.md,sha256=mvK8gVBBgoIFJqikcaReU_bUo-mTq_ECqJaDDJoQF7Q,3126
5
+ docs/Permission.md,sha256=thUJx7YRoU63Pb-eqo5l5450DrZN3QYZ36GCn8r66no,3152
6
6
  docs/Webdav.md,sha256=-Ja-BTWSY1BEMAyZycvEMNnkNTPZ49gSPzmf3Lbib70,1547
7
7
  frontend/api.js,sha256=GlQsNoZFEcy7QUUsLbXv7aP-KxRnIxM37FQHTaakGiQ,19387
8
8
  frontend/index.html,sha256=-k0bJ5FRqdl_H-O441D_H9E-iejgRCaL_z5UeYaS2qc,3384
@@ -19,7 +19,7 @@ frontend/thumb.css,sha256=rNsx766amYS2DajSQNabhpQ92gdTpNoQKmV69OKvtpI,295
19
19
  frontend/thumb.js,sha256=46ViD2TlTTWy0fx6wjoAs_5CQ4ajYB90vVzM7UO2IHw,6182
20
20
  frontend/utils.js,sha256=IYUZl77ugiXKcLxSNOWC4NSS0CdD5yRgUsDb665j0xM,2556
21
21
  lfss/api/__init__.py,sha256=8IJqrpWK1doIyVVbntvVic82A57ncwl5b0BRHX4Ri6A,6660
22
- lfss/api/connector.py,sha256=0iopAvqUiUJDjbAtAjr9ynmURnmB-Ejg3INL-877s_E,12192
22
+ lfss/api/connector.py,sha256=Duh57M3dOeG_M5UidZ4hMHK7ot1JsUC6RdXgIn6KTC8,12913
23
23
  lfss/cli/__init__.py,sha256=lPwPmqpa7EXQ4zlU7E7LOe6X2kw_xATGdwoHphUEirA,827
24
24
  lfss/cli/balance.py,sha256=fUbKKAUyaDn74f7mmxMfBL4Q4voyBLHu6Lg_g8GfMOQ,4121
25
25
  lfss/cli/cli.py,sha256=aYjB8d4k6JUd9efxZK-XOj-mlG4JeOr_0lnj2qqCiK0,8066
@@ -30,22 +30,22 @@ lfss/cli/vacuum.py,sha256=GOG72d3NYe9bYCNc3y8JecEmM-DrKlGq3JQcisv_xBg,3702
30
30
  lfss/eng/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  lfss/eng/bounded_pool.py,sha256=BI1dU-MBf82TMwJBYbjhEty7w1jIUKc5Bn9SnZ_-hoY,1288
32
32
  lfss/eng/config.py,sha256=Vni6h52Ce0njVrHZLAWFL8g34YDBdlmGrmRhpxElxQ8,868
33
- lfss/eng/connection_pool.py,sha256=4xOF1kXXGqCWeLX5ZVFALKjdY8N1VVAVSSTRfCzbj94,6141
34
- lfss/eng/database.py,sha256=9KMV-VeD8Dgd6M9RfNfAKwF5gWm763eJpJ2g5zyn_uY,48691
33
+ lfss/eng/connection_pool.py,sha256=1aq7nSgd7hB9YNV4PjD1RDRyl_moDw3ubBtSLyfgGBs,6320
34
+ lfss/eng/database.py,sha256=RYIG2506_-S84f6CsOQ6pgpr1vgPT3p1kP1FsZwTOnM,49098
35
35
  lfss/eng/datatype.py,sha256=27UB7-l9SICy5lAvKjdzpTL_GohZjzstQcr9PtAq7nM,2709
36
- lfss/eng/error.py,sha256=dAlQHXOnQcSkA2vTugJFSxcyDqoFlPucBoFpTZ7GI6w,654
36
+ lfss/eng/error.py,sha256=JGf5NV-f4rL6tNIDSAx5-l9MG8dEj7F2w_MuOjj1d1o,732
37
37
  lfss/eng/log.py,sha256=u6WRZZsE7iOx6_CV2NHh1ugea26p408FI4WstZh896A,5139
38
38
  lfss/eng/thumb.py,sha256=x9jIHHU1tskmp-TavPPcxGpbmEjCp9gbH6ZlsEfqUxY,3383
39
39
  lfss/eng/utils.py,sha256=WYoXFFi5308UWtFC8VP792gpzrVbHZZHhP3PaFjxIEY,6770
40
40
  lfss/sql/init.sql,sha256=8LjHx0TBCkBD62xFfssSeHDqKYVQQJkZAg4rSm046f4,1496
41
41
  lfss/sql/pragma.sql,sha256=uENx7xXjARmro-A3XAK8OM8v5AxDMdCCRj47f86UuXg,206
42
42
  lfss/svc/app.py,sha256=ftWCpepBx-gTSG7i-TB-IdinPPstAYYQjCgnTfeMZeI,219
43
- lfss/svc/app_base.py,sha256=PgL5MNU3QTwgIJP8CflDi9YBZ-uo4hVf74ADyWTo9yg,6742
43
+ lfss/svc/app_base.py,sha256=bTQbz945xalyB3UZLlqVBvL6JKGNQ8Fm2KpIvvucPZQ,6850
44
44
  lfss/svc/app_dav.py,sha256=D0KSgjtTktPjIhyIKG5eRmBdh5X8HYFYH151E6gzlbc,18245
45
- lfss/svc/app_native.py,sha256=N2cidZ5sIS0p9HOkPqWvpGkqe4bIA7CgGEp4CsvgZ6Q,8347
46
- lfss/svc/common_impl.py,sha256=0fjbqHWgqDhLfBEu6aC0Z5qgNt67C7z0Qroj7aV3Iq4,13830
45
+ lfss/svc/app_native.py,sha256=JbPge-F9irl26tXKAzfA5DfyjCh0Dgttflztqqrvt0A,8890
46
+ lfss/svc/common_impl.py,sha256=5ZRM24zVZpAeipgDtZUVBMFtArkydlAkn17ic_XL7v8,13733
47
47
  lfss/svc/request_log.py,sha256=v8yXEIzPjaksu76Oh5vgdbUEUrw8Kt4etLAXBWSGie8,3207
48
- lfss-0.9.5.dist-info/METADATA,sha256=fToo-wrY0_XJPbk3cte8hb_4DClyPFv3ZTzJB77G-5w,2623
49
- lfss-0.9.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
- lfss-0.9.5.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
51
- lfss-0.9.5.dist-info/RECORD,,
48
+ lfss-0.10.0.dist-info/METADATA,sha256=NewpmEw8OUj28rPkujuPi3ZySJG_JCvSEKY5JBxg6cw,2715
49
+ lfss-0.10.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
50
+ lfss-0.10.0.dist-info/entry_points.txt,sha256=VJ8svMz7RLtMCgNk99CElx7zo7M-N-z7BWDVw2HA92E,205
51
+ lfss-0.10.0.dist-info/RECORD,,
File without changes