tuspyserver 4.3.0__tar.gz → 4.3.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 (20) hide show
  1. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/PKG-INFO +1 -1
  2. tuspyserver-4.3.2/pyproject.toml +21 -0
  3. tuspyserver-4.3.0/pyproject.toml → tuspyserver-4.3.2/pyproject.toml.orig +2 -2
  4. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/lock.py +99 -22
  5. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/README.md +0 -0
  6. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/__init__.py +0 -0
  7. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/file.py +0 -0
  8. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/info.py +0 -0
  9. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/params.py +0 -0
  10. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/py.typed +0 -0
  11. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/request.py +0 -0
  12. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/router.py +0 -0
  13. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/__init__.py +0 -0
  14. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/core.py +0 -0
  15. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/creation.py +0 -0
  16. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/termination.py +0 -0
  17. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/__init__.py +0 -0
  18. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/local.py +0 -0
  19. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/s3.py +0 -0
  20. {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage_file.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: tuspyserver
3
- Version: 4.3.0
3
+ Version: 4.3.2
4
4
  Summary: A Python tus server implementation as a FastAPI router
5
5
  Author: Edi Hasaj
6
6
  Author-email: Edi Hasaj <edi.hasaj@applifyer.com>
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "tuspyserver"
3
+ version = "4.3.2"
4
+ description = "A Python tus server implementation as a FastAPI router"
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ dependencies = ["fastapi>=0.110.0"]
8
+
9
+ [[project.authors]]
10
+ name = "Edi Hasaj"
11
+ email = "edi.hasaj@applifyer.com"
12
+
13
+ [project.optional-dependencies]
14
+ s3 = ["boto3>=1.26"]
15
+
16
+ [build-system]
17
+ requires = ["uv_build>=0.6,<0.13"]
18
+ build-backend = "uv_build"
19
+
20
+ [tool.uv.workspace]
21
+ members = ["example"]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tuspyserver"
3
- version = "4.3.0"
3
+ version = "4.3.2"
4
4
  description ="A Python tus server implementation as a FastAPI router"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -16,7 +16,7 @@ dependencies = [
16
16
  s3 = ["boto3>=1.26"]
17
17
 
18
18
  [build-system]
19
- requires = ["uv_build>=0.6,<0.7"]
19
+ requires = ["uv_build>=0.6,<0.13"]
20
20
  build-backend = "uv_build"
21
21
 
22
22
 
@@ -1,11 +1,16 @@
1
1
  """
2
2
  File locking utilities for tus uploads.
3
3
 
4
- Provides advisory file locking using fcntl (Unix) to prevent race conditions
5
- during concurrent upload operations. This implementation matches tusd's approach
6
- by using separate lock files stored in a .locks directory, where each lock file
4
+ Provides advisory file locking to prevent race conditions during concurrent
5
+ upload operations. This implementation matches tusd's approach by using
6
+ separate lock files stored in a .locks directory, where each lock file
7
7
  contains the PID of the process holding the lock.
8
8
 
9
+ Locking is backed by ``fcntl.flock`` on Unix and ``msvcrt.locking`` on
10
+ Windows. Neither module is importable on every platform, so both imports are
11
+ guarded: on an exotic platform that provides neither, the module degrades to
12
+ a best-effort no-op lock instead of failing at import time.
13
+
9
14
  The lock acquisition is bounded by a timeout so a wedged shared filesystem
10
15
  (e.g. flapping Azure Files / SMB) cannot hang request workers indefinitely.
11
16
  If the lock directory is unwritable (EACCES / EROFS), we fall back to a
@@ -13,13 +18,22 @@ best-effort no-op lock so a single request fails fast instead of dragging
13
18
  all workers down.
14
19
  """
15
20
  import errno
16
- import fcntl
17
21
  import logging
18
22
  import os
19
23
  import time
20
24
  from contextlib import contextmanager
21
25
  from typing import Optional
22
26
 
27
+ try: # Unix
28
+ import fcntl
29
+ except ImportError: # pragma: no cover - platform dependent
30
+ fcntl = None # type: ignore[assignment]
31
+
32
+ try: # Windows
33
+ import msvcrt
34
+ except ImportError: # pragma: no cover - platform dependent
35
+ msvcrt = None # type: ignore[assignment]
36
+
23
37
  logger = logging.getLogger(__name__)
24
38
 
25
39
 
@@ -30,18 +44,82 @@ LOCK_POLL_INTERVAL = 0.1
30
44
 
31
45
  _FS_UNWRITABLE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS, errno.ENOSPC}
32
46
 
47
+ # Errnos meaning "someone else holds this lock" rather than a real failure.
48
+ # flock reports EAGAIN/EWOULDBLOCK; msvcrt.locking reports EACCES or
49
+ # EDEADLOCK depending on the Windows/CRT version.
50
+ _LOCK_CONTENDED_ERRNOS = {
51
+ getattr(errno, name)
52
+ for name in ("EAGAIN", "EWOULDBLOCK", "EACCES", "EDEADLK", "EDEADLOCK")
53
+ if hasattr(errno, name)
54
+ }
55
+
56
+ # Number of bytes locked on Windows. msvcrt locks a byte range from the
57
+ # current file position; one byte at offset 0 is enough for a whole-file
58
+ # advisory lock, and Windows allows locking past EOF.
59
+ _WINDOWS_LOCK_BYTES = 1
60
+
33
61
 
34
62
  class LockTimeoutError(IOError):
35
63
  """Raised when a lock cannot be acquired within the configured timeout."""
36
64
 
37
65
 
66
+ def locking_available() -> bool:
67
+ """True when this platform provides a real cross-process file lock."""
68
+ return fcntl is not None or msvcrt is not None
69
+
70
+
71
+ def _try_lock_exclusive(fd: int) -> bool:
72
+ """
73
+ Try to take an exclusive, non-blocking lock on ``fd``.
74
+
75
+ Returns True when the lock was taken (or no locking primitive exists on
76
+ this platform), False when another process holds it. Unexpected OS
77
+ errors propagate.
78
+ """
79
+ if fcntl is not None:
80
+ try:
81
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
82
+ return True
83
+ except OSError as e:
84
+ if e.errno in _LOCK_CONTENDED_ERRNOS:
85
+ return False
86
+ raise
87
+
88
+ if msvcrt is not None:
89
+ try:
90
+ os.lseek(fd, 0, os.SEEK_SET)
91
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, _WINDOWS_LOCK_BYTES)
92
+ return True
93
+ except OSError as e:
94
+ if e.errno in _LOCK_CONTENDED_ERRNOS:
95
+ return False
96
+ raise
97
+
98
+ logger.warning(
99
+ "No file locking primitive available on this platform; "
100
+ "proceeding without a cross-process lock"
101
+ )
102
+ return True
103
+
104
+
105
+ def _unlock(fd: int) -> None:
106
+ """Release a lock previously taken by :func:`_try_lock_exclusive`."""
107
+ if fcntl is not None:
108
+ fcntl.flock(fd, fcntl.LOCK_UN)
109
+ elif msvcrt is not None:
110
+ os.lseek(fd, 0, os.SEEK_SET)
111
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, _WINDOWS_LOCK_BYTES)
112
+
113
+
38
114
  class FileLock:
39
115
  """
40
- Advisory file lock using fcntl (Unix) with separate lock files.
116
+ Advisory file lock with separate lock files.
41
117
 
42
118
  Matches tusd's filelocker approach by creating separate .lock files
43
119
  in a .locks directory. Each lock file contains the PID of the process
44
120
  holding the lock, allowing for lock cleanup if a process crashes.
121
+
122
+ Uses ``fcntl.flock`` on Unix and ``msvcrt.locking`` on Windows.
45
123
  """
46
124
 
47
125
  def __init__(self, file_path: str, locks_dir: Optional[str] = None):
@@ -139,22 +217,21 @@ class FileLock:
139
217
  deadline = time.monotonic() + max(timeout, 0.0) if blocking else None
140
218
  while True:
141
219
  try:
142
- fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
143
- return True
144
- except OSError as e:
145
- if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
146
- self._close_fd()
147
- raise
148
- if not blocking:
149
- self._close_fd()
150
- return False
151
- if time.monotonic() >= deadline:
152
- self._close_fd()
153
- raise LockTimeoutError(
154
- f"Timed out after {timeout:.1f}s waiting for lock "
155
- f"{self.lock_file_path}"
156
- )
157
- time.sleep(LOCK_POLL_INTERVAL)
220
+ if _try_lock_exclusive(self._lock_fd):
221
+ return True
222
+ except OSError:
223
+ self._close_fd()
224
+ raise
225
+ if not blocking:
226
+ self._close_fd()
227
+ return False
228
+ if time.monotonic() >= deadline:
229
+ self._close_fd()
230
+ raise LockTimeoutError(
231
+ f"Timed out after {timeout:.1f}s waiting for lock "
232
+ f"{self.lock_file_path}"
233
+ )
234
+ time.sleep(LOCK_POLL_INTERVAL)
158
235
 
159
236
  def _close_fd(self) -> None:
160
237
  if self._lock_fd is not None:
@@ -171,7 +248,7 @@ class FileLock:
171
248
  return
172
249
  if self._lock_fd is not None:
173
250
  try:
174
- fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
251
+ _unlock(self._lock_fd)
175
252
  except Exception as e:
176
253
  logger.warning("Error unlocking %s: %s", self.lock_file_path, e)
177
254
  self._close_fd()
File without changes