igntui 0.2.0__tar.gz → 0.3.1__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.
- {igntui-0.2.0 → igntui-0.3.1}/PKG-INFO +1 -1
- igntui-0.3.1/pyproject.toml +148 -0
- igntui-0.2.0/pyproject.toml → igntui-0.3.1/pyproject.toml.orig +1 -1
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/__init__.py +1 -1
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/cache_cmd.py +22 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/completion_cmd.py +7 -2
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/parser.py +5 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/cache.py +37 -21
- {igntui-0.2.0 → igntui-0.3.1}/LICENSE +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/README.md +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/__main__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/app.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/base.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/generate_cmd.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/list_cmd.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/test_cmd.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/commands/tui_cmd.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/cli/setup.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/client.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/errors.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/rate_limiter.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/request_handler.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/response.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/api/types.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/config.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/managed_block.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/project_config.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/repo_config.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/search.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/core/usage.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/main.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/py.typed +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/actions.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/app.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/curses_setup.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/event_handler.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/lifecycle.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/renderer.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/state.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/tui/updates.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/__init__.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/base_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/base_panel.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/confirm_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/content_panel.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/diff_preview_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/export_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/help_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/save_dialog.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/search_panel.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/selected_panel.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/splash.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/status_bar.py +0 -0
- {igntui-0.2.0 → igntui-0.3.1}/src/igntui/ui/components/templates_panel.py +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "igntui"
|
|
3
|
+
version = "0.3.1"
|
|
4
|
+
description = "Interactive Terminal User Interface for GitIgnore template generation"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "GPL-3.0-or-later"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"windows-curses>=2.4.1; sys_platform == 'win32'",
|
|
11
|
+
"pyfiglet>=1.0.2",
|
|
12
|
+
"tomli-w>=1.0.0",
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"gitignore",
|
|
16
|
+
"tui",
|
|
17
|
+
"terminal",
|
|
18
|
+
"git",
|
|
19
|
+
"templates",
|
|
20
|
+
"cli",
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 4 - Beta",
|
|
24
|
+
"Environment :: Console :: Curses",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
33
|
+
"Topic :: Terminals",
|
|
34
|
+
"Topic :: Utilities",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Mohammad Abu Mattar"
|
|
39
|
+
email = "mohammad.khaled@outlook.com"
|
|
40
|
+
|
|
41
|
+
[[project.maintainers]]
|
|
42
|
+
name = "Mohammad Abu Mattar"
|
|
43
|
+
email = "mohammad.khaled@outlook.com"
|
|
44
|
+
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://github.com/MKAbuMattar/igntui"
|
|
47
|
+
Repository = "https://github.com/MKAbuMattar/igntui"
|
|
48
|
+
Documentation = "https://github.com/MKAbuMattar/igntui/blob/main/README.md"
|
|
49
|
+
Issues = "https://github.com/MKAbuMattar/igntui/issues"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
igntui = "igntui.main:cli_main"
|
|
53
|
+
gitignore-tui = "igntui.main:tui_main"
|
|
54
|
+
|
|
55
|
+
[project.optional-dependencies]
|
|
56
|
+
dev = [
|
|
57
|
+
"pytest>=7.0.0",
|
|
58
|
+
"pytest-cov>=4.0.0",
|
|
59
|
+
"pytest-mock>=3.10.0",
|
|
60
|
+
"ruff>=0.1.0",
|
|
61
|
+
]
|
|
62
|
+
test = [
|
|
63
|
+
"pytest>=7.0.0",
|
|
64
|
+
"pytest-cov>=4.0.0",
|
|
65
|
+
"pytest-mock>=3.10.0",
|
|
66
|
+
"freezegun>=1.4.0",
|
|
67
|
+
]
|
|
68
|
+
windows = ["windows-curses>=2.4.1; sys_platform == 'win32'"]
|
|
69
|
+
|
|
70
|
+
[build-system]
|
|
71
|
+
requires = ["uv_build>=0.9.5,<0.13.0"]
|
|
72
|
+
build-backend = "uv_build"
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 100
|
|
76
|
+
target-version = "py311"
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = [
|
|
80
|
+
"E",
|
|
81
|
+
"W",
|
|
82
|
+
"F",
|
|
83
|
+
"I",
|
|
84
|
+
"B",
|
|
85
|
+
"C4",
|
|
86
|
+
"UP",
|
|
87
|
+
]
|
|
88
|
+
ignore = [
|
|
89
|
+
"E501",
|
|
90
|
+
"B008",
|
|
91
|
+
"C901",
|
|
92
|
+
"E402",
|
|
93
|
+
"W291",
|
|
94
|
+
"W293",
|
|
95
|
+
"C408",
|
|
96
|
+
"C414",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
[tool.ruff.lint.per-file-ignores]
|
|
100
|
+
"__init__.py" = ["F401"]
|
|
101
|
+
|
|
102
|
+
[tool.mypy]
|
|
103
|
+
python_version = "3.11"
|
|
104
|
+
warn_return_any = true
|
|
105
|
+
warn_unused_configs = true
|
|
106
|
+
disallow_untyped_defs = false
|
|
107
|
+
ignore_missing_imports = true
|
|
108
|
+
|
|
109
|
+
[[tool.mypy.overrides]]
|
|
110
|
+
module = [
|
|
111
|
+
"curses",
|
|
112
|
+
"curses.*",
|
|
113
|
+
"windows_curses",
|
|
114
|
+
"windows_curses.*",
|
|
115
|
+
]
|
|
116
|
+
ignore_missing_imports = true
|
|
117
|
+
ignore_errors = true
|
|
118
|
+
|
|
119
|
+
[tool.ty.environment]
|
|
120
|
+
python-version = "3.11"
|
|
121
|
+
|
|
122
|
+
[tool.ty.src]
|
|
123
|
+
include = ["src/igntui"]
|
|
124
|
+
|
|
125
|
+
[tool.pytest.ini_options]
|
|
126
|
+
testpaths = ["tests"]
|
|
127
|
+
python_files = ["test_*.py"]
|
|
128
|
+
addopts = "-v --strict-markers"
|
|
129
|
+
markers = ["integration: tests that hit the network or filesystem extensively"]
|
|
130
|
+
|
|
131
|
+
[tool.coverage.run]
|
|
132
|
+
source = ["src/igntui"]
|
|
133
|
+
omit = [
|
|
134
|
+
"*/tests/*",
|
|
135
|
+
"*/__main__.py",
|
|
136
|
+
"*/tui/curses_setup.py",
|
|
137
|
+
"*/tui/renderer.py",
|
|
138
|
+
"*/ui/*",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
[tool.coverage.report]
|
|
142
|
+
exclude_lines = [
|
|
143
|
+
"pragma: no cover",
|
|
144
|
+
"raise NotImplementedError",
|
|
145
|
+
"if __name__ == .__main__.:",
|
|
146
|
+
"if TYPE_CHECKING:",
|
|
147
|
+
]
|
|
148
|
+
show_missing = true
|
|
@@ -5,7 +5,7 @@ import platform
|
|
|
5
5
|
import sys
|
|
6
6
|
from collections.abc import Callable
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.3.1"
|
|
9
9
|
__author__ = "Mohammad Abu Mattar"
|
|
10
10
|
__email__ = "mohammad.khaled@outlook.com"
|
|
11
11
|
__description__ = """A professional TUI application for generating .gitignore files from templates
|
|
@@ -17,6 +17,11 @@ class CacheCommand(CLICommand):
|
|
|
17
17
|
|
|
18
18
|
clear_parser = subparsers.add_parser("clear", help="Clear cache")
|
|
19
19
|
clear_parser.add_argument("--force", action="store_true", help="Skip confirmation prompt")
|
|
20
|
+
clear_parser.add_argument(
|
|
21
|
+
"--expired",
|
|
22
|
+
action="store_true",
|
|
23
|
+
help="Remove only entries past their TTL, keeping everything still valid",
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
subparsers.add_parser("stats", help="Show cache statistics")
|
|
22
27
|
subparsers.add_parser("info", help="Show cache information")
|
|
@@ -33,6 +38,8 @@ class CacheCommand(CLICommand):
|
|
|
33
38
|
elif args.cache_action == "stats":
|
|
34
39
|
return self._show_stats(cache)
|
|
35
40
|
elif args.cache_action == "clear":
|
|
41
|
+
if getattr(args, "expired", False):
|
|
42
|
+
return self._clear_expired(cache)
|
|
36
43
|
return self._clear_cache(cache, args.force)
|
|
37
44
|
else:
|
|
38
45
|
print(f"Unknown cache action: {args.cache_action}")
|
|
@@ -85,6 +92,21 @@ class CacheCommand(CLICommand):
|
|
|
85
92
|
|
|
86
93
|
return 0
|
|
87
94
|
|
|
95
|
+
def _clear_expired(self, cache: "CacheManager") -> int:
|
|
96
|
+
"""Sweep only what is past its TTL.
|
|
97
|
+
|
|
98
|
+
Nothing prunes expired files on its own any more: reading every entry at
|
|
99
|
+
startup to find them cost every command ~19 ms over a realistic cache.
|
|
100
|
+
Re-requesting a template set overwrites its stale entry in place, so this
|
|
101
|
+
is for sets that were cached once and never asked for again.
|
|
102
|
+
"""
|
|
103
|
+
removed = cache.cleanup_expired()
|
|
104
|
+
if removed:
|
|
105
|
+
print(f"Removed {removed} expired {'entry' if removed == 1 else 'entries'}")
|
|
106
|
+
else:
|
|
107
|
+
print("No expired entries")
|
|
108
|
+
return 0
|
|
109
|
+
|
|
88
110
|
def _clear_cache(self, cache: "CacheManager", force: bool = False) -> int:
|
|
89
111
|
if not force:
|
|
90
112
|
response = input("Clear cache? This will remove all cached data. (y/N): ")
|
|
@@ -34,7 +34,7 @@ _igntui_complete() {
|
|
|
34
34
|
tui) COMPREPLY=( $(compgen -W "--no-splash" -- "$cur") ); return ;;
|
|
35
35
|
list) COMPREPLY=( $(compgen -W "--filter --count" -- "$cur") ); return ;;
|
|
36
36
|
generate) COMPREPLY=( $(compgen -W "--output --append --force --dry-run --no-sidecar" -- "$cur") ); return ;;
|
|
37
|
-
cache) COMPREPLY=( $(compgen -W "info stats clear --force" -- "$cur") ); return ;;
|
|
37
|
+
cache) COMPREPLY=( $(compgen -W "info stats clear --force --expired" -- "$cur") ); return ;;
|
|
38
38
|
test) COMPREPLY=( $(compgen -W "--timeout" -- "$cur") ); return ;;
|
|
39
39
|
completion) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
40
40
|
"") COMPREPLY=( $(compgen -W "$subcommands $global_flags" -- "$cur") ); return ;;
|
|
@@ -73,7 +73,10 @@ _igntui() {
|
|
|
73
73
|
'--force[overwrite without prompt]' \\
|
|
74
74
|
'--dry-run[print without writing]' \\
|
|
75
75
|
'--no-sidecar[skip igntui.cfg.toml]' ;;
|
|
76
|
-
cache)
|
|
76
|
+
cache) _arguments \\
|
|
77
|
+
'1:action:(info stats clear)' \\
|
|
78
|
+
'--force[skip confirmation]' \\
|
|
79
|
+
'--expired[only entries past their TTL]' ;;
|
|
77
80
|
test) _arguments '--timeout[seconds]:seconds:' ;;
|
|
78
81
|
completion) _values 'shell' bash zsh fish ;;
|
|
79
82
|
esac
|
|
@@ -110,6 +113,8 @@ complete -c igntui -n "__fish_seen_subcommand_from generate" -l dry-run -d "Prin
|
|
|
110
113
|
complete -c igntui -n "__fish_seen_subcommand_from generate" -l no-sidecar -d "Skip sidecar"
|
|
111
114
|
complete -c igntui -n "__fish_seen_subcommand_from generate" -l force -d "Overwrite without prompt"
|
|
112
115
|
complete -c igntui -n "__fish_seen_subcommand_from cache" -a "info stats clear"
|
|
116
|
+
complete -c igntui -n "__fish_seen_subcommand_from cache" -l force -d "Skip confirmation"
|
|
117
|
+
complete -c igntui -n "__fish_seen_subcommand_from cache" -l expired -d "Only expired entries"
|
|
113
118
|
complete -c igntui -n "__fish_seen_subcommand_from completion" -a "bash zsh fish"
|
|
114
119
|
"""
|
|
115
120
|
|
|
@@ -117,6 +117,11 @@ def create_command_parser(
|
|
|
117
117
|
|
|
118
118
|
cache_clear_parser = cache_subparsers.add_parser("clear", help="Clear all cache")
|
|
119
119
|
cache_clear_parser.add_argument("--force", action="store_true", help="Skip confirmation prompt")
|
|
120
|
+
cache_clear_parser.add_argument(
|
|
121
|
+
"--expired",
|
|
122
|
+
action="store_true",
|
|
123
|
+
help="Remove only entries past their TTL, keeping everything still valid",
|
|
124
|
+
)
|
|
120
125
|
cache_subparsers.add_parser("stats", help="Show cache statistics")
|
|
121
126
|
cache_subparsers.add_parser("info", help="Show cache information")
|
|
122
127
|
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
9
|
+
import tempfile
|
|
8
10
|
import time
|
|
9
11
|
from dataclasses import asdict, dataclass
|
|
10
12
|
from pathlib import Path
|
|
@@ -49,8 +51,19 @@ class CacheManager:
|
|
|
49
51
|
|
|
50
52
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
51
53
|
self._purge_legacy_content_keys()
|
|
52
|
-
self._load_persistent_cache()
|
|
53
54
|
|
|
55
|
+
# Deliberately not reading the cache into memory here. `get()` already
|
|
56
|
+
# falls back to disk and promotes what it finds, so an eager load bought
|
|
57
|
+
# nothing except latency — and every command pays it, because
|
|
58
|
+
# GitIgnoreAPI constructs a manager on the way to doing anything. Over a
|
|
59
|
+
# realistic cache (300 content blobs, ~3.5 MiB) it measured ~19 ms of
|
|
60
|
+
# open + json.loads before the process could start its actual work.
|
|
61
|
+
#
|
|
62
|
+
# The eager pass also deleted expired files. Losing that is nearly free:
|
|
63
|
+
# a content key is a hash of its template set, so re-requesting the same
|
|
64
|
+
# set overwrites the stale entry in place, and `get()` deletes any
|
|
65
|
+
# expired entry it touches. Only sets never asked for again linger, and
|
|
66
|
+
# `igntui cache clear --expired` sweeps those.
|
|
54
67
|
logger.debug("Initialized cache manager with directory: %s", self.cache_dir)
|
|
55
68
|
|
|
56
69
|
def _purge_legacy_content_keys(self) -> None:
|
|
@@ -196,25 +209,6 @@ class CacheManager:
|
|
|
196
209
|
**self._stats,
|
|
197
210
|
}
|
|
198
211
|
|
|
199
|
-
def _load_persistent_cache(self) -> None:
|
|
200
|
-
try:
|
|
201
|
-
loaded_count = 0
|
|
202
|
-
for cache_file in self.cache_dir.glob("*.cache"):
|
|
203
|
-
key = cache_file.stem
|
|
204
|
-
entry = self._load_disk_cache(key)
|
|
205
|
-
|
|
206
|
-
if entry and not entry.is_expired():
|
|
207
|
-
self._memory_cache[key] = entry
|
|
208
|
-
loaded_count += 1
|
|
209
|
-
elif entry:
|
|
210
|
-
cache_file.unlink()
|
|
211
|
-
|
|
212
|
-
if loaded_count > 0:
|
|
213
|
-
logger.info("Loaded %d cache entries from disk", loaded_count)
|
|
214
|
-
|
|
215
|
-
except Exception as e:
|
|
216
|
-
logger.warning("Failed to load persistent cache: %s", e)
|
|
217
|
-
|
|
218
212
|
def _load_disk_cache(self, key: str) -> CacheEntry | None:
|
|
219
213
|
cache_file = self.cache_dir / f"{key}.cache"
|
|
220
214
|
|
|
@@ -236,16 +230,38 @@ class CacheManager:
|
|
|
236
230
|
return None
|
|
237
231
|
|
|
238
232
|
def _save_disk_cache(self, key: str, entry: CacheEntry) -> None:
|
|
233
|
+
"""Write an entry to disk atomically.
|
|
234
|
+
|
|
235
|
+
Writing in place meant a crash, a full disk, or two processes saving the
|
|
236
|
+
same key could leave truncated JSON behind. That was survivable — a read
|
|
237
|
+
catches the JSONDecodeError and unlinks the file — but the entry was lost
|
|
238
|
+
and the failure was silent. Writing a temp file in the same directory and
|
|
239
|
+
renaming makes the swap atomic on POSIX and Windows: a reader sees either
|
|
240
|
+
the old entry or the new one, never half of either.
|
|
241
|
+
"""
|
|
239
242
|
cache_file = self.cache_dir / f"{key}.cache"
|
|
243
|
+
tmp_path: str | None = None
|
|
240
244
|
|
|
241
245
|
try:
|
|
242
|
-
|
|
246
|
+
# Same directory, so os.replace is a rename rather than a cross-device copy.
|
|
247
|
+
fd, tmp_path = tempfile.mkstemp(dir=self.cache_dir, suffix=".tmp")
|
|
248
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
243
249
|
json.dump(asdict(entry), f, separators=(",", ":"))
|
|
250
|
+
os.replace(tmp_path, cache_file)
|
|
251
|
+
tmp_path = None
|
|
244
252
|
|
|
245
253
|
self._stats["disk_writes"] += 1
|
|
246
254
|
|
|
247
255
|
except (OSError, TypeError) as e:
|
|
248
256
|
logger.warning("Failed to save cache file %s: %s", cache_file, e)
|
|
257
|
+
finally:
|
|
258
|
+
# A serialisation failure leaves the temp file behind; the previous
|
|
259
|
+
# good entry is still in place, which is the point.
|
|
260
|
+
if tmp_path is not None:
|
|
261
|
+
try:
|
|
262
|
+
os.unlink(tmp_path)
|
|
263
|
+
except OSError:
|
|
264
|
+
pass
|
|
249
265
|
|
|
250
266
|
def _delete_disk_cache(self, key: str) -> bool:
|
|
251
267
|
cache_file = self.cache_dir / f"{key}.cache"
|
|
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
|
|
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
|