flowmark 0.4.4__tar.gz → 0.4.6__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 (37) hide show
  1. {flowmark-0.4.4 → flowmark-0.4.6}/.gitignore +2 -0
  2. {flowmark-0.4.4 → flowmark-0.4.6}/PKG-INFO +3 -3
  3. {flowmark-0.4.4 → flowmark-0.4.6}/pyproject.toml +2 -2
  4. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/line_wrappers.py +54 -2
  5. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/markdown_filling.py +3 -2
  6. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/reformat_api.py +1 -3
  7. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/text_wrapping.py +10 -4
  8. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_wrapping.py +50 -0
  9. {flowmark-0.4.4 → flowmark-0.4.6}/tests/testdocs/testdoc.orig.md +46 -1
  10. {flowmark-0.4.4 → flowmark-0.4.6}/tests/testdocs/testdoc.out.cleaned.md +68 -0
  11. {flowmark-0.4.4 → flowmark-0.4.6}/tests/testdocs/testdoc.out.plain.md +68 -0
  12. {flowmark-0.4.4 → flowmark-0.4.6}/tests/testdocs/testdoc.out.semantic.md +68 -0
  13. {flowmark-0.4.4 → flowmark-0.4.6}/uv.lock +52 -55
  14. {flowmark-0.4.4 → flowmark-0.4.6}/.copier-answers.yml +0 -0
  15. {flowmark-0.4.4 → flowmark-0.4.6}/.github/workflows/ci.yml +0 -0
  16. {flowmark-0.4.4 → flowmark-0.4.6}/.github/workflows/publish.yml +0 -0
  17. {flowmark-0.4.4 → flowmark-0.4.6}/LICENSE +0 -0
  18. {flowmark-0.4.4 → flowmark-0.4.6}/Makefile +0 -0
  19. {flowmark-0.4.4 → flowmark-0.4.6}/README.md +0 -0
  20. {flowmark-0.4.4 → flowmark-0.4.6}/development.md +0 -0
  21. {flowmark-0.4.4 → flowmark-0.4.6}/devtools/lint.py +0 -0
  22. {flowmark-0.4.4 → flowmark-0.4.6}/installation.md +0 -0
  23. {flowmark-0.4.4 → flowmark-0.4.6}/poetry.lock +0 -0
  24. {flowmark-0.4.4 → flowmark-0.4.6}/publishing.md +0 -0
  25. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/__init__.py +0 -0
  26. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/cleanups.py +0 -0
  27. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/cli.py +0 -0
  28. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/custom_marko.py +0 -0
  29. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/frontmatter.py +0 -0
  30. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/py.typed +0 -0
  31. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/sentence_split_regex.py +0 -0
  32. {flowmark-0.4.4 → flowmark-0.4.6}/src/flowmark/text_filling.py +0 -0
  33. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_cleanups.py +0 -0
  34. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_filling.py +0 -0
  35. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_frontmatter.py +0 -0
  36. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_ref_docs.py +0 -0
  37. {flowmark-0.4.4 → flowmark-0.4.6}/tests/test_sentences.py +0 -0
@@ -5,6 +5,7 @@ tmp/
5
5
  trash/
6
6
  attic/
7
7
  .kash/
8
+ **/.claude/settings.local.json
8
9
 
9
10
  # Byte-compiled / optimized / DLL files
10
11
  __pycache__/
@@ -177,3 +178,4 @@ cython_debug/
177
178
 
178
179
  # PyPI configuration file
179
180
  .pypirc
181
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmark
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Better line wrapping and formatting for plaintext and Markdown
5
5
  Project-URL: Repository, https://github.com/jlevy/flowmark
6
6
  Author-email: Joshua Levy <joshua@cal.berkeley.edu>
@@ -16,9 +16,9 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Python: <4.0,>=3.10
19
- Requires-Dist: marko>=2.1.2
19
+ Requires-Dist: marko>=2.1.3
20
20
  Requires-Dist: regex>=2024.11.6
21
- Requires-Dist: strif>=2.0.0
21
+ Requires-Dist: strif>=3.0.1
22
22
  Requires-Dist: typing-extensions>=4.12.2
23
23
  Description-Content-Type: text/markdown
24
24
 
@@ -35,9 +35,9 @@ classifiers = [
35
35
  ]
36
36
 
37
37
  dependencies = [
38
- "marko>=2.1.2",
38
+ "marko>=2.1.3",
39
39
  "regex>=2024.11.6",
40
- "strif>=2.0.0",
40
+ "strif>=3.0.1",
41
41
  "typing-extensions>=4.12.2",
42
42
  ]
43
43
 
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from collections.abc import Callable
2
3
  from typing import Protocol
3
4
 
@@ -27,6 +28,51 @@ def split_sentences_no_min_length(text: str) -> list[str]:
27
28
  return split_sentences_regex(text, min_length=0)
28
29
 
29
30
 
31
+ _line_break_re = re.compile(r"\\\n| \n")
32
+
33
+
34
+ def split_markdown_hard_breaks(text: str) -> list[str]:
35
+ """
36
+ Split text by explicit Markdown line breaks.
37
+ """
38
+ return _line_break_re.split(text)
39
+
40
+
41
+ def _add_markdown_hard_break_handling(base_wrapper: LineWrapper) -> LineWrapper:
42
+ """
43
+ Augments a LineWrapper to first split the text by Markdown hard breaks,
44
+ wrap each segment using the base_wrapper, and then rejoin them with
45
+ a normalized Markdown hard break (backslash-newline).
46
+ """
47
+
48
+ def enhanced_wrapper(text: str, initial_indent: str, subsequent_indent: str) -> str:
49
+ segments = split_markdown_hard_breaks(text)
50
+
51
+ # Handle empty input.
52
+ if not segments:
53
+ return ""
54
+ # Handle single segment (no hard line breaks).
55
+ if len(segments) == 1:
56
+ return base_wrapper(text, initial_indent, subsequent_indent)
57
+
58
+ wrapped_segments: list[str] = []
59
+
60
+ for i, segment in enumerate(segments):
61
+ is_first = i == 0
62
+ is_last = i == len(segments) - 1
63
+
64
+ cur_initial_indent = initial_indent if is_first else subsequent_indent
65
+ wrapped_segment = base_wrapper(segment, cur_initial_indent, subsequent_indent)
66
+ if is_last:
67
+ wrapped_segments.append(wrapped_segment)
68
+ else:
69
+ wrapped_segments.append(wrapped_segment + "\\")
70
+
71
+ return "\n".join(wrapped_segments)
72
+
73
+ return enhanced_wrapper
74
+
75
+
30
76
  def line_wrap_to_width(
31
77
  width: int = DEFAULT_WRAP_WIDTH,
32
78
  len_fn: Callable[[str], int] = DEFAULT_LEN_FUNCTION,
@@ -46,7 +92,10 @@ def line_wrap_to_width(
46
92
  is_markdown=is_markdown,
47
93
  )
48
94
 
49
- return line_wrapper
95
+ if is_markdown:
96
+ return _add_markdown_hard_break_handling(line_wrapper)
97
+ else:
98
+ return line_wrapper
50
99
 
51
100
 
52
101
  def line_wrap_by_sentence(
@@ -106,4 +155,7 @@ def line_wrap_by_sentence(
106
155
 
107
156
  return "\n".join(lines)
108
157
 
109
- return line_wrapper
158
+ if is_markdown:
159
+ return _add_markdown_hard_break_handling(line_wrapper)
160
+ else:
161
+ return line_wrapper
@@ -30,10 +30,11 @@ def _normalize_html_comments(text: str, break_str: str = "\n\n") -> str:
30
30
 
31
31
  # Small hack to avoid changing frontmatter format, for the rare corner
32
32
  # case where Markdown contains HTML-style frontmatter.
33
+ # https://github.com/jlevy/frontmatter-format
33
34
  def not_frontmatter(text: str) -> bool:
34
- return "<!---" not in text
35
+ return "<!---" not in text # Three dashes for frontmatter format.
35
36
 
36
- # TODO: Probably want do this for <div>s too.
37
+ # TODO: Perhaps could add support to do this for block elements like <div>s too?
37
38
  return _ensure_surrounding_breaks(
38
39
  text, [("<!--", "-->")], break_str=break_str, filter=not_frontmatter
39
40
  )
@@ -63,12 +63,10 @@ def reformat_file(
63
63
  make_parents: Whether to make parent directories if they don't exist.
64
64
  """
65
65
  read_stdin = path == "-"
66
- write_stdout = output == "-"
66
+ write_stdout = output == "-" or not output
67
67
 
68
68
  if inplace and read_stdin:
69
69
  raise ValueError("Cannot use `inplace` with stdin")
70
- if inplace and output and output != path:
71
- raise ValueError("Cannot use `inplace` with an output path")
72
70
 
73
71
  if read_stdin:
74
72
  text = sys.stdin.read()
@@ -105,18 +105,24 @@ def wrap_paragraph_lines(
105
105
  len_fn: Callable[[str], int] = DEFAULT_LEN_FUNCTION,
106
106
  is_markdown: bool = False,
107
107
  ) -> list[str]:
108
- """
108
+ r"""
109
109
  Wrap a single paragraph of text, returning a list of wrapped lines.
110
110
  Rewritten to simplify and generalize Python's textwrap.py.
111
- Set `is_markdown` to True when wrapping markdown text to automatically
112
- escape special markdown characters at the start of wrapped lines.
111
+
112
+ Set `is_markdown` to True when wrapping markdown text to enable Markdown mode.
113
+ This automatically escapes special markdown characters at the start of wrapped
114
+ lines. It also will preserve line breaks with an explicit Markdown line break,
115
+ i.e. "\\\n" (backslash-newline) or " \n" (two spaces followed by newline) at the
116
+ end of the line. Hard line breaks are normalized to always use "\\\n" as the line
117
+ break.
113
118
  """
119
+ lines: list[str] = []
120
+
114
121
  if replace_whitespace:
115
122
  text = re.sub(r"\s+", " ", text)
116
123
 
117
124
  words = splitter(text)
118
125
 
119
- lines: list[str] = []
120
126
  current_line: list[str] = []
121
127
  current_width = initial_column
122
128
  first_line = True
@@ -221,3 +221,53 @@ def test_wrap_width():
221
221
  print(wrapped)
222
222
  print([len(line) for line in wrapped])
223
223
  assert all(len(line) <= width for line in wrapped)
224
+
225
+
226
+ def test_line_wrap_to_width_with_markdown_breaks():
227
+ from flowmark.line_wrappers import line_wrap_to_width
228
+
229
+ # Get a markdown-aware line wrapper
230
+ wrapper = line_wrap_to_width(width=80, is_markdown=True)
231
+
232
+ # Test trailing space line breaks
233
+ text_with_spaces = "This line ends with spaces \nThis is a new line"
234
+ wrapped_spaces = wrapper(text_with_spaces, initial_indent="", subsequent_indent="")
235
+ assert wrapped_spaces == "This line ends with spaces\\\nThis is a new line"
236
+
237
+ # Test backslash line breaks
238
+ text_with_backslash = "This line ends with backslash\\\nThis is a new line"
239
+ wrapped_backslash = wrapper(text_with_backslash, initial_indent="", subsequent_indent="")
240
+ assert wrapped_backslash == "This line ends with backslash\\\nThis is a new line"
241
+
242
+ # Test wrapping with indentation
243
+ indented_wrapper = line_wrap_to_width(width=40, is_markdown=True)
244
+ long_text = (
245
+ "This is a very long line that will be wrapped and it ends with a line break \n"
246
+ "Next line with content that continues"
247
+ )
248
+ wrapped_long = indented_wrapper(long_text, initial_indent=" ", subsequent_indent=" ")
249
+ assert wrapped_long == (
250
+ " This is a very long line that will be\n"
251
+ " wrapped and it ends with a line\n"
252
+ " break\\\n"
253
+ " Next line with content that\n"
254
+ " continues"
255
+ )
256
+
257
+ # Test different indentation for segments
258
+ mixed_indent_wrapper = line_wrap_to_width(width=30, is_markdown=True)
259
+ mixed_indent_text = "First segment \nSecond segment\\\nThird segment"
260
+ wrapped_mixed_indent = mixed_indent_wrapper(
261
+ mixed_indent_text, initial_indent="* ", subsequent_indent=" "
262
+ )
263
+ assert wrapped_mixed_indent == ("* First segment\\\n Second segment\\\n Third segment")
264
+
265
+ # Test empty segments
266
+ empty_segment_text = "Before \n\\\nAfter"
267
+ wrapped_empty = wrapper(empty_segment_text, initial_indent="", subsequent_indent="")
268
+ assert wrapped_empty == "Before\\\n\\\nAfter"
269
+
270
+ # Test single segment (no line breaks)
271
+ single_segment = "Text with no breaks"
272
+ wrapped_single = wrapper(single_segment, initial_indent="> ", subsequent_indent=" ")
273
+ assert wrapped_single == "> Text with no breaks"
@@ -116,7 +116,7 @@ the Center for American Entrepreneurship.)
116
116
 
117
117
  ### A few paragraphs and bullets
118
118
 
119
- A good heuristic is to assume your readers will be **100% intelligent and 100% ignorant**. Of course, in reality, varied experience exists within an individual reader. Most people may already know *something*, and some people are quicker learners than others. Even world class experts most likely only really know parts of a subject, and contributors may have practical experience in one role--perhaps they have been an entrepreneur, for example, but not an investor. So writing with the assumption that each reader could be both has a variety of advantages:
119
+ A good heuristic is to assume your readers will be **100% intelligent and 100% ignorant**. Of course, in reality, varied experience exists within an individual reader. Most people may already know *something*, and some people are quicker learners than others. Even world class experts most likely only really know parts of a subject, and contributors may have practical experience in one role--perhaps they have been an entrepreneur, for example, but not an investor. So writing with the assumption that each reader could be both has a variety of advantages:
120
120
 
121
121
 
122
122
  - Assuming 100% ignorance will help people who think they know a lot about a subject understand **what they didn't know they didn't know**, and fill in the gaps in their knowledge. It encourages contribution from experts and people with practical experience.
@@ -297,6 +297,9 @@ Recommendations:
297
297
 
298
298
  ### **3.6 Comparative Features Matrix**
299
299
 
300
+ This line has a two-space line break.
301
+ And this is a regular line.
302
+
300
303
  The following table summarizes the native capabilities of the primary platforms evaluated against the core requirements:
301
304
 
302
305
  | **Feature** | **Vercel** | **Netlify** | **Cloudflare Pages** | **AWS (S3/CloudFront)** |
@@ -310,6 +313,48 @@ The following table summarizes the native capabilities of the primary platforms
310
313
  | **Programmatic Invalidation API** | Automatic / Limited | Automatic / Limited | Yes (Cloudflare API) | Yes (CloudFront API) 15-16 |
311
314
  | **Basic Cost Model** | Per User / Usage 5 | Per User / Usage 5 | Generous Free Tier / Usage | Usage-Based Components 20-64 |
312
315
 
316
+ ## **3\. Platform\-Specific Mechanisms**
317
+
318
+ Each major operating system provides its own set of system calls, APIs, and conventions for managing temporary files and directories.
319
+
320
+ ### **3\.1\. Linux**
321
+
322
+ Linux offers a rich set of POSIX\-compliant and Linux\-specific mechanisms.
323
+
324
+ * **APIs:**
325
+ + mkstemp(3\): Generates a unique filename based on a template (prefixXXXXXX), creates the file with 0600 permissions, opens it, and returns a file descriptor. It uses O\_EXCL for atomic creation, preventing race conditions.<sup>19</sup> The caller is responsible for unlinking the file.
326
+ + mkdtemp(3\): Similar to mkstemp but creates a unique directory with 0700 permissions based on a template (prefixXXXXXX), returning the path.<sup>19</sup> The caller must remove the directory.
327
+ + tmpfile(3\): Creates an unnamed temporary file opened in wb\+ mode (binary read/write), automatically deleted when closed or on process termination.<sup>19</sup> While convenient, POSIX notes potential permission issues and recommends mkstemp followed by
328
+ fdopen for multithreaded apps to avoid leaking file descriptors.<sup>59</sup>
329
+
330
+ * **Recommended Python Approach for Atomic Writes:**
331
+ 1. **Use a Library:** Employing a dedicated library like atomicwriter or atomicfile is generally the most robust approach, as they handle temporary file creation in the correct location, atomic renaming (os.replace), and error cleanup across platforms.
332
+ 2. **Manual Implementation (if necessary):**
333
+ + Determine the destination directory: dest\_dir = os.path.dirname(dest\_path)
334
+ + Create the temporary file in the destination directory: temp\_file = tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False) (use appropriate mode, e.g., wb for binary).
335
+ + Use a try...finally block to ensure cleanup:\
336
+ Python\
337
+ temp\_file = None\
338
+ try:\
339
+ temp\_file = tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False, prefix='tmp', suffix='.tmp')\
340
+ # Write data to temp\_file.file (or temp\_file directly in older Pythons)\
341
+ temp\_file.write(...)\
342
+ # Ensure data is on disk before renaming\
343
+ temp\_file.flush()\
344
+ os.fsync(temp\_file.fileno())\
345
+ # Close the file handle before renaming\
346
+ temp\_file.close()\
347
+ # Atomic rename/replace\
348
+ os.replace(temp\_file.name, dest\_path)\
349
+ temp\_file = None # Prevent cleanup in finally block if successful\
350
+ finally:\
351
+ if temp\_file is not None and os.path.exists(temp\_file.name):\
352
+ try:\
353
+ os.unlink(temp\_file.name)\
354
+ except OSError:\
355
+ # Log error, but continue\
356
+ pass\
357
+
313
358
  ### Boldface, italics, and links
314
359
 
315
360
  X [New York City](https://en.wikipedia.org/wiki/New_York_City).
@@ -471,6 +471,9 @@ Recommendations:
471
471
 
472
472
  ### 3.6 Comparative Features Matrix
473
473
 
474
+ This line has a two-space line break.\
475
+ And this is a regular line.
476
+
474
477
  The following table summarizes the native capabilities of the primary platforms
475
478
  evaluated against the core requirements:
476
479
 
@@ -485,6 +488,71 @@ evaluated against the core requirements:
485
488
  | **Programmatic Invalidation API** | Automatic / Limited | Automatic / Limited | Yes (Cloudflare API) | Yes (CloudFront API) 15-16 |
486
489
  | **Basic Cost Model** | Per User / Usage 5 | Per User / Usage 5 | Generous Free Tier / Usage | Usage-Based Components 20-64 |
487
490
 
491
+ ## 3\. Platform\-Specific Mechanisms
492
+
493
+ Each major operating system provides its own set of system calls, APIs, and conventions
494
+ for managing temporary files and directories.
495
+
496
+ ### 3\.1\. Linux
497
+
498
+ Linux offers a rich set of POSIX\-compliant and Linux\-specific mechanisms.
499
+
500
+ * **APIs:**
501
+
502
+ + mkstemp(3\): Generates a unique filename based on a template (prefixXXXXXX), creates
503
+ the file with 0600 permissions, opens it, and returns a file descriptor.
504
+ It uses O\_EXCL for atomic creation, preventing race conditions.<sup>19</sup> The
505
+ caller is responsible for unlinking the file.
506
+
507
+ + mkdtemp(3\): Similar to mkstemp but creates a unique directory with 0700 permissions
508
+ based on a template (prefixXXXXXX), returning the path.<sup>19</sup> The caller must
509
+ remove the directory.
510
+
511
+ + tmpfile(3\): Creates an unnamed temporary file opened in wb\+ mode (binary
512
+ read/write), automatically deleted when closed or on process
513
+ termination.<sup>19</sup> While convenient, POSIX notes potential permission issues
514
+ and recommends mkstemp followed by fdopen for multithreaded apps to avoid leaking
515
+ file descriptors.<sup>59</sup>
516
+
517
+ * **Recommended Python Approach for Atomic Writes:**
518
+
519
+ 1. **Use a Library:** Employing a dedicated library like atomicwriter or atomicfile is
520
+ generally the most robust approach, as they handle temporary file creation in the
521
+ correct location, atomic renaming (os.replace), and error cleanup across platforms.
522
+
523
+ 2. **Manual Implementation (if necessary):**
524
+
525
+ + Determine the destination directory: dest\_dir = os.path.dirname(dest\_path)
526
+
527
+ + Create the temporary file in the destination directory: temp\_file =
528
+ tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False) (use
529
+ appropriate mode, e.g., wb for binary).
530
+
531
+
532
+ + Use a try...finally block to ensure cleanup:\
533
+ Python\
534
+ temp\_file = None\
535
+ try:\
536
+ temp\_file = tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False,
537
+ prefix='tmp', suffix='.tmp')\
538
+ # Write data to temp\_file.file (or temp\_file directly in older Pythons)\
539
+ temp\_file.write(...)\
540
+ # Ensure data is on disk before renaming\
541
+ temp\_file.flush()\
542
+ os.fsync(temp\_file.fileno())\
543
+ # Close the file handle before renaming\
544
+ temp\_file.close()\
545
+ # Atomic rename/replace\
546
+ os.replace(temp\_file.name, dest\_path)\
547
+ temp\_file = None # Prevent cleanup in finally block if successful\
548
+ finally:\
549
+ if temp\_file is not None and os.path.exists(temp\_file.name):\
550
+ try:\
551
+ os.unlink(temp\_file.name)\
552
+ except OSError:\
553
+ # Log error, but continue\
554
+ pass\
555
+
488
556
  ### Boldface, italics, and links
489
557
 
490
558
  X [New York City](https://en.wikipedia.org/wiki/New_York_City). XX
@@ -448,6 +448,9 @@ Recommendations:
448
448
 
449
449
  ### **3.6 Comparative Features Matrix**
450
450
 
451
+ This line has a two-space line break.\
452
+ And this is a regular line.
453
+
451
454
  The following table summarizes the native capabilities of the primary platforms
452
455
  evaluated against the core requirements:
453
456
 
@@ -462,6 +465,71 @@ evaluated against the core requirements:
462
465
  | **Programmatic Invalidation API** | Automatic / Limited | Automatic / Limited | Yes (Cloudflare API) | Yes (CloudFront API) 15-16 |
463
466
  | **Basic Cost Model** | Per User / Usage 5 | Per User / Usage 5 | Generous Free Tier / Usage | Usage-Based Components 20-64 |
464
467
 
468
+ ## **3\. Platform\-Specific Mechanisms**
469
+
470
+ Each major operating system provides its own set of system calls, APIs, and conventions
471
+ for managing temporary files and directories.
472
+
473
+ ### **3\.1\. Linux**
474
+
475
+ Linux offers a rich set of POSIX\-compliant and Linux\-specific mechanisms.
476
+
477
+ * **APIs:**
478
+
479
+ + mkstemp(3\): Generates a unique filename based on a template (prefixXXXXXX), creates
480
+ the file with 0600 permissions, opens it, and returns a file descriptor. It uses
481
+ O\_EXCL for atomic creation, preventing race conditions.<sup>19</sup> The caller is
482
+ responsible for unlinking the file.
483
+
484
+ + mkdtemp(3\): Similar to mkstemp but creates a unique directory with 0700 permissions
485
+ based on a template (prefixXXXXXX), returning the path.<sup>19</sup> The caller must
486
+ remove the directory.
487
+
488
+ + tmpfile(3\): Creates an unnamed temporary file opened in wb\+ mode (binary
489
+ read/write), automatically deleted when closed or on process
490
+ termination.<sup>19</sup> While convenient, POSIX notes potential permission issues
491
+ and recommends mkstemp followed by fdopen for multithreaded apps to avoid leaking
492
+ file descriptors.<sup>59</sup>
493
+
494
+ * **Recommended Python Approach for Atomic Writes:**
495
+
496
+ 1. **Use a Library:** Employing a dedicated library like atomicwriter or atomicfile is
497
+ generally the most robust approach, as they handle temporary file creation in the
498
+ correct location, atomic renaming (os.replace), and error cleanup across platforms.
499
+
500
+ 2. **Manual Implementation (if necessary):**
501
+
502
+ + Determine the destination directory: dest\_dir = os.path.dirname(dest\_path)
503
+
504
+ + Create the temporary file in the destination directory: temp\_file =
505
+ tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False) (use
506
+ appropriate mode, e.g., wb for binary).
507
+
508
+
509
+ + Use a try...finally block to ensure cleanup:\
510
+ Python\
511
+ temp\_file = None\
512
+ try:\
513
+ temp\_file = tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False,
514
+ prefix='tmp', suffix='.tmp')\
515
+ # Write data to temp\_file.file (or temp\_file directly in older Pythons)\
516
+ temp\_file.write(...)\
517
+ # Ensure data is on disk before renaming\
518
+ temp\_file.flush()\
519
+ os.fsync(temp\_file.fileno())\
520
+ # Close the file handle before renaming\
521
+ temp\_file.close()\
522
+ # Atomic rename/replace\
523
+ os.replace(temp\_file.name, dest\_path)\
524
+ temp\_file = None # Prevent cleanup in finally block if successful\
525
+ finally:\
526
+ if temp\_file is not None and os.path.exists(temp\_file.name):\
527
+ try:\
528
+ os.unlink(temp\_file.name)\
529
+ except OSError:\
530
+ # Log error, but continue\
531
+ pass\
532
+
465
533
  ### Boldface, italics, and links
466
534
 
467
535
  X [New York City](https://en.wikipedia.org/wiki/New_York_City). XX
@@ -471,6 +471,9 @@ Recommendations:
471
471
 
472
472
  ### **3.6 Comparative Features Matrix**
473
473
 
474
+ This line has a two-space line break.\
475
+ And this is a regular line.
476
+
474
477
  The following table summarizes the native capabilities of the primary platforms
475
478
  evaluated against the core requirements:
476
479
 
@@ -485,6 +488,71 @@ evaluated against the core requirements:
485
488
  | **Programmatic Invalidation API** | Automatic / Limited | Automatic / Limited | Yes (Cloudflare API) | Yes (CloudFront API) 15-16 |
486
489
  | **Basic Cost Model** | Per User / Usage 5 | Per User / Usage 5 | Generous Free Tier / Usage | Usage-Based Components 20-64 |
487
490
 
491
+ ## **3\. Platform\-Specific Mechanisms**
492
+
493
+ Each major operating system provides its own set of system calls, APIs, and conventions
494
+ for managing temporary files and directories.
495
+
496
+ ### **3\.1\. Linux**
497
+
498
+ Linux offers a rich set of POSIX\-compliant and Linux\-specific mechanisms.
499
+
500
+ * **APIs:**
501
+
502
+ + mkstemp(3\): Generates a unique filename based on a template (prefixXXXXXX), creates
503
+ the file with 0600 permissions, opens it, and returns a file descriptor.
504
+ It uses O\_EXCL for atomic creation, preventing race conditions.<sup>19</sup> The
505
+ caller is responsible for unlinking the file.
506
+
507
+ + mkdtemp(3\): Similar to mkstemp but creates a unique directory with 0700 permissions
508
+ based on a template (prefixXXXXXX), returning the path.<sup>19</sup> The caller must
509
+ remove the directory.
510
+
511
+ + tmpfile(3\): Creates an unnamed temporary file opened in wb\+ mode (binary
512
+ read/write), automatically deleted when closed or on process
513
+ termination.<sup>19</sup> While convenient, POSIX notes potential permission issues
514
+ and recommends mkstemp followed by fdopen for multithreaded apps to avoid leaking
515
+ file descriptors.<sup>59</sup>
516
+
517
+ * **Recommended Python Approach for Atomic Writes:**
518
+
519
+ 1. **Use a Library:** Employing a dedicated library like atomicwriter or atomicfile is
520
+ generally the most robust approach, as they handle temporary file creation in the
521
+ correct location, atomic renaming (os.replace), and error cleanup across platforms.
522
+
523
+ 2. **Manual Implementation (if necessary):**
524
+
525
+ + Determine the destination directory: dest\_dir = os.path.dirname(dest\_path)
526
+
527
+ + Create the temporary file in the destination directory: temp\_file =
528
+ tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False) (use
529
+ appropriate mode, e.g., wb for binary).
530
+
531
+
532
+ + Use a try...finally block to ensure cleanup:\
533
+ Python\
534
+ temp\_file = None\
535
+ try:\
536
+ temp\_file = tempfile.NamedTemporaryFile(mode='w', dir=dest\_dir, delete=False,
537
+ prefix='tmp', suffix='.tmp')\
538
+ # Write data to temp\_file.file (or temp\_file directly in older Pythons)\
539
+ temp\_file.write(...)\
540
+ # Ensure data is on disk before renaming\
541
+ temp\_file.flush()\
542
+ os.fsync(temp\_file.fileno())\
543
+ # Close the file handle before renaming\
544
+ temp\_file.close()\
545
+ # Atomic rename/replace\
546
+ os.replace(temp\_file.name, dest\_path)\
547
+ temp\_file = None # Prevent cleanup in finally block if successful\
548
+ finally:\
549
+ if temp\_file is not None and os.path.exists(temp\_file.name):\
550
+ try:\
551
+ os.unlink(temp\_file.name)\
552
+ except OSError:\
553
+ # Log error, but continue\
554
+ pass\
555
+
488
556
  ### Boldface, italics, and links
489
557
 
490
558
  X [New York City](https://en.wikipedia.org/wiki/New_York_City). XX
@@ -4,14 +4,14 @@ requires-python = ">=3.10, <4.0"
4
4
 
5
5
  [[package]]
6
6
  name = "basedpyright"
7
- version = "1.28.3"
7
+ version = "1.29.1"
8
8
  source = { registry = "https://pypi.org/simple" }
9
9
  dependencies = [
10
10
  { name = "nodejs-wheel-binaries" },
11
11
  ]
12
- sdist = { url = "https://files.pythonhosted.org/packages/37/bb/d5b68bfacecaee6abccef32871fe9d93e74d48ff9f89da967d52f3965b2c/basedpyright-1.28.3.tar.gz", hash = "sha256:c860a42b963a3d5eaa7141612653af0b3735ca1a1bf7b15c25efa8bdcba6f1eb", size = 21620897 }
12
+ sdist = { url = "https://files.pythonhosted.org/packages/b9/18/f5e488eac4960ad9a2e71b95f0d91cf93a982c7f68aa90e4e0554f0bc37e/basedpyright-1.29.1.tar.gz", hash = "sha256:06bbe6c3b50ab4af20f80e154049477a50d8b81d2522eadbc9f472f2f92cd44b", size = 21773469 }
13
13
  wheels = [
14
- { url = "https://files.pythonhosted.org/packages/43/7a/e303376c43f676d38a9df5bca9ce34760ed87707fcaaba813c3e6fffc343/basedpyright-1.28.3-py3-none-any.whl", hash = "sha256:1787283dd3a4c9698708b8dee59026e1bcebaf954c7d4711a47c3fe1f884da7f", size = 11506366 },
14
+ { url = "https://files.pythonhosted.org/packages/95/1b/1bb837bbb7e259928f33d3c105dfef4f5349ef08b3ef45576801256e3234/basedpyright-1.29.1-py3-none-any.whl", hash = "sha256:b7eb65b9d4aaeeea29a349ac494252032a75a364942d0ac466d7f07ddeacc786", size = 11397959 },
15
15
  ]
16
16
 
17
17
  [[package]]
@@ -63,9 +63,9 @@ dev = [
63
63
 
64
64
  [package.metadata]
65
65
  requires-dist = [
66
- { name = "marko", specifier = ">=2.1.2" },
66
+ { name = "marko", specifier = ">=2.1.3" },
67
67
  { name = "regex", specifier = ">=2024.11.6" },
68
- { name = "strif", specifier = ">=2.0.0" },
68
+ { name = "strif", specifier = ">=3.0.1" },
69
69
  { name = "typing-extensions", specifier = ">=4.12.2" },
70
70
  ]
71
71
 
@@ -81,14 +81,11 @@ dev = [
81
81
 
82
82
  [[package]]
83
83
  name = "funlog"
84
- version = "0.2.0"
84
+ version = "0.2.1"
85
85
  source = { registry = "https://pypi.org/simple" }
86
- dependencies = [
87
- { name = "strif" },
88
- ]
89
- sdist = { url = "https://files.pythonhosted.org/packages/f3/f9/0e345fbcf27983884b8a16439e25c51d70ecdbafb0645d021dc032a540da/funlog-0.2.0.tar.gz", hash = "sha256:315383e29dc866e7d1a656425f34eb12338e60de17304109b7b765b9176eb12f", size = 23147 }
86
+ sdist = { url = "https://files.pythonhosted.org/packages/22/46/4ab8e6d87ab6aae6f1eafdd2f891c009e95e295bec2df8cd0b2f06870417/funlog-0.2.1.tar.gz", hash = "sha256:16f89e9c499077b374b4986a7c7dfc9fcd21ef40eb757fefff1a3631fdddf16f", size = 25691 }
90
87
  wheels = [
91
- { url = "https://files.pythonhosted.org/packages/b8/fe/0333cd8b831a0d689f300fb7a498b0df768c8eb810bd039ad63af27eb17a/funlog-0.2.0-py3-none-any.whl", hash = "sha256:d76f054aaa5c4a761cb24810a5b2b16ce1829128709d9f739a355bfbbaefbdcc", size = 8096 },
88
+ { url = "https://files.pythonhosted.org/packages/d8/af/420b08227bd021f80008520aa20a001120e71042342801d0c782a6615a39/funlog-0.2.1-py3-none-any.whl", hash = "sha256:eed8d206c21ee8dc96137b4df51689470682d4700f6f99a1a6133a0e065f3798", size = 9399 },
92
89
  ]
93
90
 
94
91
  [[package]]
@@ -114,11 +111,11 @@ wheels = [
114
111
 
115
112
  [[package]]
116
113
  name = "marko"
117
- version = "2.1.2"
114
+ version = "2.1.3"
118
115
  source = { registry = "https://pypi.org/simple" }
119
- sdist = { url = "https://files.pythonhosted.org/packages/8c/38/6ea5d8600b94432656c669816a479580d9f1c49ef6b426282f4ba261ae9b/marko-2.1.2.tar.gz", hash = "sha256:a9170006b879376e6845c91b1ae3dce2992772954b99b70175ff888537186011", size = 142593 }
116
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/e7/b50627a7321668d0c30b193aa494fb59526548ff3e61d376a93ffbb692f0/marko-2.1.3.tar.gz", hash = "sha256:31aacb14867328f054cc39f884212907822a43d6a30cd75b0767e001a5e2f9fc", size = 142700 }
120
117
  wheels = [
121
- { url = "https://files.pythonhosted.org/packages/ef/9b/3dbfbe6ee255b1c37a37e2a6046adb2e77763a020591dae63e5005a2c8d7/marko-2.1.2-py3-none-any.whl", hash = "sha256:c14aa7a77468aaaf53cf056dcd3d32398b9df4c3fb81f5e120dd37cbb9f8c859", size = 42089 },
118
+ { url = "https://files.pythonhosted.org/packages/fc/43/c82e8f528887cec56f5afa9152c921b01f98deeffaf9e1bcc3e54e90c291/marko-2.1.3-py3-none-any.whl", hash = "sha256:b4125d44b94606d6f13ddc77fef8cc4c87f70d54bc7d52d6547958b9f998a9d5", size = 42187 },
122
119
  ]
123
120
 
124
121
  [[package]]
@@ -132,27 +129,27 @@ wheels = [
132
129
 
133
130
  [[package]]
134
131
  name = "nodejs-wheel-binaries"
135
- version = "22.14.0"
132
+ version = "22.15.0"
136
133
  source = { registry = "https://pypi.org/simple" }
137
- sdist = { url = "https://files.pythonhosted.org/packages/d7/c7/4fd3871d2b7fd5122216245e273201ab98eda92bbd6fe9ad04846b758c56/nodejs_wheel_binaries-22.14.0.tar.gz", hash = "sha256:c1dc43713598c7310d53795c764beead861b8c5021fe4b1366cb912ce1a4c8bf", size = 8055 }
134
+ sdist = { url = "https://files.pythonhosted.org/packages/45/5b/6c5f973765b96793d4e4d03684bcbd273b17e471ecc7e9bec4c32b595ebd/nodejs_wheel_binaries-22.15.0.tar.gz", hash = "sha256:ff81aa2a79db279c2266686ebcb829b6634d049a5a49fc7dc6921e4f18af9703", size = 8054 }
138
135
  wheels = [
139
- { url = "https://files.pythonhosted.org/packages/61/b6/66ef4ef75ea7389ea788f2d5505bf9a8e5c3806d56c7a90cf46a6942f1cf/nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d8ab8690516a3e98458041286e3f0d6458de176d15c14f205c3ea2972131420d", size = 50326597 },
140
- { url = "https://files.pythonhosted.org/packages/7d/78/023d91a293ba73572a643bc89d11620d189f35f205a309dd8296aa45e69a/nodejs_wheel_binaries-22.14.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:b2f200f23b3610bdbee01cf136279e005ffdf8ee74557aa46c0940a7867956f6", size = 51158258 },
141
- { url = "https://files.pythonhosted.org/packages/af/86/324f6342c79e5034a13319b02ba9ed1f4ac8813af567d223c9a9e56cd338/nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0877832abd7a9c75c8c5caafa37f986c9341ee025043c2771213d70c4c1defa", size = 57180264 },
142
- { url = "https://files.pythonhosted.org/packages/6d/9f/42bdaab26137e31732bff00147b9aca2185d475b5752b57a443e6c7ba93f/nodejs_wheel_binaries-22.14.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fded5a70a8a55c2135e67bd580d8b7f2e94fcbafcc679b6a2d5b92f88373d69", size = 57693251 },
143
- { url = "https://files.pythonhosted.org/packages/ab/d7/94f8f269aa86cf35f9ed2b70d09aca48dc971fb5656fdc4a3b69364b189f/nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c1ade6f3ece458b40c02e89c91d5103792a9f18aaad5026da533eb0dcb87090e", size = 58841717 },
144
- { url = "https://files.pythonhosted.org/packages/2d/a0/43b7316eaf22b4ee9bfb897ee36c724efceac7b89d7d1bedca28057b7be1/nodejs_wheel_binaries-22.14.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34fa5ed4cf3f65cbfbe9b45c407ffc2fc7d97a06cd8993e6162191ff81f29f48", size = 59808791 },
145
- { url = "https://files.pythonhosted.org/packages/10/0a/814491f751a25136e37de68a2728c9a9e3c1d20494aba5ff3c230d5f9c2d/nodejs_wheel_binaries-22.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:ca7023276327455988b81390fa6bbfa5191c1da7fc45bc57c7abc281ba9967e9", size = 40478921 },
146
- { url = "https://files.pythonhosted.org/packages/f4/5c/cab444afaa387dceac8debb817b52fd00596efcd2d54506c27311c6fe6a8/nodejs_wheel_binaries-22.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:fd59c8e9a202221e316febe1624a1ae3b42775b7fb27737bf12ec79565983eaf", size = 36206637 },
136
+ { url = "https://files.pythonhosted.org/packages/d3/a8/a32e5bb99e95c536e7dac781cffab1e7e9f8661b8ee296b93df77e4df7f9/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:aa16366d48487fff89446fb237693e777aa2ecd987208db7d4e35acc40c3e1b1", size = 50514526 },
137
+ { url = "https://files.pythonhosted.org/packages/05/e8/eb024dbb3a7d3b98c8922d1c306be989befad4d2132292954cb902f43b07/nodejs_wheel_binaries-22.15.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:a54bb3fee9170003fa8abc69572d819b2b1540344eff78505fcc2129a9175596", size = 51409179 },
138
+ { url = "https://files.pythonhosted.org/packages/3f/0f/baa968456c3577e45c7d0e3715258bd175dcecc67b683a41a5044d5dae40/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:867121ccf99d10523f6878a26db86e162c4939690e24cfb5bea56d01ea696c93", size = 57364460 },
139
+ { url = "https://files.pythonhosted.org/packages/2f/a2/977f63cd07ed8fc27bc0d0cd72e801fc3691ffc8cd40a51496ff18a6d0a2/nodejs_wheel_binaries-22.15.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ab0fbcda2ddc8aab7db1505d72cb958f99324b3834c4543541a305e02bfe860", size = 57889101 },
140
+ { url = "https://files.pythonhosted.org/packages/67/7f/57b9c24a4f0d25490527b043146aa0fdff2d8fdc82f90667cdaf6f00cfc9/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2bde1d8e00cd955b9ce9ee9ac08309923e2778a790ee791b715e93e487e74bfd", size = 59190817 },
141
+ { url = "https://files.pythonhosted.org/packages/fd/7f/970acbe33b81c22b3c7928f52e32347030aa46d23d779cf781cf9a9cf557/nodejs_wheel_binaries-22.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:acdd4ef73b6701aab9fbe02ac5e104f208a5e3c300402fa41ad7bc7f49499fbf", size = 60220316 },
142
+ { url = "https://files.pythonhosted.org/packages/07/4c/030243c04bb60f0de66c2d7ee3be289c6d28ef09113c06ffa417bdfedf8f/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:51deaf13ee474e39684ce8c066dfe86240edb94e7241950ca789befbbbcbd23d", size = 40718853 },
143
+ { url = "https://files.pythonhosted.org/packages/1f/49/011d472814af4fabeaab7d7ce3d5a1a635a3dadc23ae404d1f546839ecb3/nodejs_wheel_binaries-22.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:01a3fe4d60477f93bf21a44219db33548c75d7fed6dc6e6f4c05cf0adf015609", size = 36436645 },
147
144
  ]
148
145
 
149
146
  [[package]]
150
147
  name = "packaging"
151
- version = "24.2"
148
+ version = "25.0"
152
149
  source = { registry = "https://pypi.org/simple" }
153
- sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
150
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
154
151
  wheels = [
155
- { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
152
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
156
153
  ]
157
154
 
158
155
  [[package]]
@@ -261,50 +258,50 @@ wheels = [
261
258
 
262
259
  [[package]]
263
260
  name = "rich"
264
- version = "13.9.4"
261
+ version = "14.0.0"
265
262
  source = { registry = "https://pypi.org/simple" }
266
263
  dependencies = [
267
264
  { name = "markdown-it-py" },
268
265
  { name = "pygments" },
269
266
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
270
267
  ]
271
- sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
268
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 }
272
269
  wheels = [
273
- { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
270
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 },
274
271
  ]
275
272
 
276
273
  [[package]]
277
274
  name = "ruff"
278
- version = "0.11.2"
275
+ version = "0.11.9"
279
276
  source = { registry = "https://pypi.org/simple" }
280
- sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
277
+ sdist = { url = "https://files.pythonhosted.org/packages/f5/e7/e55dda1c92cdcf34b677ebef17486669800de01e887b7831a1b8fdf5cb08/ruff-0.11.9.tar.gz", hash = "sha256:ebd58d4f67a00afb3a30bf7d383e52d0e036e6195143c6db7019604a05335517", size = 4132134 }
281
278
  wheels = [
282
- { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
283
- { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
284
- { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
285
- { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
286
- { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
287
- { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
288
- { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
289
- { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
290
- { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
291
- { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
292
- { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
293
- { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
294
- { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
295
- { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
296
- { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
297
- { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
298
- { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
279
+ { url = "https://files.pythonhosted.org/packages/fb/71/75dfb7194fe6502708e547941d41162574d1f579c4676a8eb645bf1a6842/ruff-0.11.9-py3-none-linux_armv6l.whl", hash = "sha256:a31a1d143a5e6f499d1fb480f8e1e780b4dfdd580f86e05e87b835d22c5c6f8c", size = 10335453 },
280
+ { url = "https://files.pythonhosted.org/packages/74/fc/ad80c869b1732f53c4232bbf341f33c5075b2c0fb3e488983eb55964076a/ruff-0.11.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:66bc18ca783b97186a1f3100e91e492615767ae0a3be584e1266aa9051990722", size = 11072566 },
281
+ { url = "https://files.pythonhosted.org/packages/87/0d/0ccececef8a0671dae155cbf7a1f90ea2dd1dba61405da60228bbe731d35/ruff-0.11.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:bd576cd06962825de8aece49f28707662ada6a1ff2db848d1348e12c580acbf1", size = 10435020 },
282
+ { url = "https://files.pythonhosted.org/packages/52/01/e249e1da6ad722278094e183cbf22379a9bbe5f21a3e46cef24ccab76e22/ruff-0.11.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1d18b4be8182cc6fddf859ce432cc9631556e9f371ada52f3eaefc10d878de", size = 10593935 },
283
+ { url = "https://files.pythonhosted.org/packages/ed/9a/40cf91f61e3003fe7bd43f1761882740e954506c5a0f9097b1cff861f04c/ruff-0.11.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0f3f46f759ac623e94824b1e5a687a0df5cd7f5b00718ff9c24f0a894a683be7", size = 10172971 },
284
+ { url = "https://files.pythonhosted.org/packages/61/12/d395203de1e8717d7a2071b5a340422726d4736f44daf2290aad1085075f/ruff-0.11.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f34847eea11932d97b521450cf3e1d17863cfa5a94f21a056b93fb86f3f3dba2", size = 11748631 },
285
+ { url = "https://files.pythonhosted.org/packages/66/d6/ef4d5eba77677eab511644c37c55a3bb8dcac1cdeb331123fe342c9a16c9/ruff-0.11.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f33b15e00435773df97cddcd263578aa83af996b913721d86f47f4e0ee0ff271", size = 12409236 },
286
+ { url = "https://files.pythonhosted.org/packages/c5/8f/5a2c5fc6124dd925a5faf90e1089ee9036462118b619068e5b65f8ea03df/ruff-0.11.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b27613a683b086f2aca8996f63cb3dd7bc49e6eccf590563221f7b43ded3f65", size = 11881436 },
287
+ { url = "https://files.pythonhosted.org/packages/39/d1/9683f469ae0b99b95ef99a56cfe8c8373c14eba26bd5c622150959ce9f64/ruff-0.11.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e0d88756e63e8302e630cee3ce2ffb77859797cc84a830a24473939e6da3ca6", size = 13982759 },
288
+ { url = "https://files.pythonhosted.org/packages/4e/0b/c53a664f06e0faab596397867c6320c3816df479e888fe3af63bc3f89699/ruff-0.11.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537c82c9829d7811e3aa680205f94c81a2958a122ac391c0eb60336ace741a70", size = 11541985 },
289
+ { url = "https://files.pythonhosted.org/packages/23/a0/156c4d7e685f6526a636a60986ee4a3c09c8c4e2a49b9a08c9913f46c139/ruff-0.11.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:440ac6a7029f3dee7d46ab7de6f54b19e34c2b090bb4f2480d0a2d635228f381", size = 10465775 },
290
+ { url = "https://files.pythonhosted.org/packages/43/d5/88b9a6534d9d4952c355e38eabc343df812f168a2c811dbce7d681aeb404/ruff-0.11.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:71c539bac63d0788a30227ed4d43b81353c89437d355fdc52e0cda4ce5651787", size = 10170957 },
291
+ { url = "https://files.pythonhosted.org/packages/f0/b8/2bd533bdaf469dc84b45815ab806784d561fab104d993a54e1852596d581/ruff-0.11.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c67117bc82457e4501473c5f5217d49d9222a360794bfb63968e09e70f340abd", size = 11143307 },
292
+ { url = "https://files.pythonhosted.org/packages/2f/d9/43cfba291788459b9bfd4e09a0479aa94d05ab5021d381a502d61a807ec1/ruff-0.11.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e4b78454f97aa454586e8a5557facb40d683e74246c97372af3c2d76901d697b", size = 11603026 },
293
+ { url = "https://files.pythonhosted.org/packages/22/e6/7ed70048e89b01d728ccc950557a17ecf8df4127b08a56944b9d0bae61bc/ruff-0.11.9-py3-none-win32.whl", hash = "sha256:7fe1bc950e7d7b42caaee2a8a3bc27410547cc032c9558ee2e0f6d3b209e845a", size = 10548627 },
294
+ { url = "https://files.pythonhosted.org/packages/90/36/1da5d566271682ed10f436f732e5f75f926c17255c9c75cefb77d4bf8f10/ruff-0.11.9-py3-none-win_amd64.whl", hash = "sha256:52edaa4a6d70f8180343a5b7f030c7edd36ad180c9f4d224959c2d689962d964", size = 11634340 },
295
+ { url = "https://files.pythonhosted.org/packages/40/f7/70aad26e5877c8f7ee5b161c4c9fa0100e63fc4c944dc6d97b9c7e871417/ruff-0.11.9-py3-none-win_arm64.whl", hash = "sha256:bcf42689c22f2e240f496d0c183ef2c6f7b35e809f12c1db58f75d9aa8d630ca", size = 10741080 },
299
296
  ]
300
297
 
301
298
  [[package]]
302
299
  name = "strif"
303
- version = "2.1.0"
300
+ version = "3.0.1"
304
301
  source = { registry = "https://pypi.org/simple" }
305
- sdist = { url = "https://files.pythonhosted.org/packages/80/98/7cef4d6fcd9273f209c0b732ef76e972477d906576c27691ac6a9f2dc9f4/strif-2.1.0.tar.gz", hash = "sha256:42b19114e8c5e5763936233b4bb5b3f12d421bb138fcd93fa025ab7361cffea7", size = 27093 }
302
+ sdist = { url = "https://files.pythonhosted.org/packages/5c/63/bf90e5632aea866c1bbd2567f04a7026191728c648ecf44ff489829898a3/strif-3.0.1.tar.gz", hash = "sha256:a312ea9540062ccd386132ac2eece93c0c4adaeb25a5f5da11eed7867b1ff31e", size = 33981 }
306
303
  wheels = [
307
- { url = "https://files.pythonhosted.org/packages/9e/95/579444185a9d0800afb009730ea3a2f9f8835be8e67a1edd161efaa89823/strif-2.1.0-py3-none-any.whl", hash = "sha256:eab5372414e75ffbe2c6ac7faffca691fdeaf2c36b5b012c3b7286f07a3b551c", size = 11679 },
304
+ { url = "https://files.pythonhosted.org/packages/a4/fd/94ebfa518f46b473d238a8ad5837ce75cfe4ea0ce5a91544a88198d8a8d3/strif-3.0.1-py3-none-any.whl", hash = "sha256:5dca44af3ff2f8a947a0398b619d138014f4d29a5aaf6c7e73fe7bec5f5973da", size = 18446 },
308
305
  ]
309
306
 
310
307
  [[package]]
@@ -348,9 +345,9 @@ wheels = [
348
345
 
349
346
  [[package]]
350
347
  name = "typing-extensions"
351
- version = "4.12.2"
348
+ version = "4.13.2"
352
349
  source = { registry = "https://pypi.org/simple" }
353
- sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
350
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
354
351
  wheels = [
355
- { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
352
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
356
353
  ]
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