anidb-client 1.0.0__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 (64) hide show
  1. anidb_client-1.0.0/.claude/agents/scripts/ci-drift-detector.mjs +195 -0
  2. anidb_client-1.0.0/.claude/skills/adr/scripts/generate_index.py +154 -0
  3. anidb_client-1.0.0/.claude/skills/spec/scripts/generate_index.py +145 -0
  4. anidb_client-1.0.0/.gitignore +67 -0
  5. anidb_client-1.0.0/LICENSE +675 -0
  6. anidb_client-1.0.0/PKG-INFO +512 -0
  7. anidb_client-1.0.0/README.md +488 -0
  8. anidb_client-1.0.0/pyproject.toml +207 -0
  9. anidb_client-1.0.0/scripts/__init__.py +6 -0
  10. anidb_client-1.0.0/scripts/conventional_commit.py +196 -0
  11. anidb_client-1.0.0/scripts/release_tag.py +147 -0
  12. anidb_client-1.0.0/src/anidb_client/ISO-639-2_utf-8.txt +486 -0
  13. anidb_client-1.0.0/src/anidb_client/__init__.py +287 -0
  14. anidb_client-1.0.0/src/anidb_client/anames.py +466 -0
  15. anidb_client-1.0.0/src/anidb_client/animeobjs.py +2035 -0
  16. anidb_client-1.0.0/src/anidb_client/commands.py +573 -0
  17. anidb_client-1.0.0/src/anidb_client/db.py +523 -0
  18. anidb_client-1.0.0/src/anidb_client/errors.py +76 -0
  19. anidb_client-1.0.0/src/anidb_client/fileinfo.py +167 -0
  20. anidb_client-1.0.0/src/anidb_client/link.py +926 -0
  21. anidb_client-1.0.0/src/anidb_client/mapper.py +460 -0
  22. anidb_client-1.0.0/src/anidb_client/ratelimit.py +206 -0
  23. anidb_client-1.0.0/src/anidb_client/responses.py +2009 -0
  24. anidb_client-1.0.0/tests/__init__.py +6 -0
  25. anidb_client-1.0.0/tests/conftest.py +189 -0
  26. anidb_client-1.0.0/tests/factories.py +163 -0
  27. anidb_client-1.0.0/tests/fake_anidb.py +171 -0
  28. anidb_client-1.0.0/tests/integration/__init__.py +7 -0
  29. anidb_client-1.0.0/tests/integration/test_link.py +957 -0
  30. anidb_client-1.0.0/tests/integration/test_schema_postgres.py +246 -0
  31. anidb_client-1.0.0/tests/objectlayer.py +123 -0
  32. anidb_client-1.0.0/tests/schema_snapshot.py +135 -0
  33. anidb_client-1.0.0/tests/schema_snapshots/postgresql.sql +148 -0
  34. anidb_client-1.0.0/tests/schema_snapshots/sqlite.sql +138 -0
  35. anidb_client-1.0.0/tests/test_network_guard.py +76 -0
  36. anidb_client-1.0.0/tests/unit/__init__.py +1 -0
  37. anidb_client-1.0.0/tests/unit/test_attribute_resolution.py +116 -0
  38. anidb_client-1.0.0/tests/unit/test_cache_freshness.py +217 -0
  39. anidb_client-1.0.0/tests/unit/test_cache_session_lifecycle.py +113 -0
  40. anidb_client-1.0.0/tests/unit/test_commands.py +322 -0
  41. anidb_client-1.0.0/tests/unit/test_conventional_commit.py +213 -0
  42. anidb_client-1.0.0/tests/unit/test_db.py +475 -0
  43. anidb_client-1.0.0/tests/unit/test_enum_converters.py +335 -0
  44. anidb_client-1.0.0/tests/unit/test_episode_from_filename.py +154 -0
  45. anidb_client-1.0.0/tests/unit/test_external_mapping.py +252 -0
  46. anidb_client-1.0.0/tests/unit/test_fanart_http.py +167 -0
  47. anidb_client-1.0.0/tests/unit/test_file_identity.py +167 -0
  48. anidb_client-1.0.0/tests/unit/test_file_response_decoding.py +61 -0
  49. anidb_client-1.0.0/tests/unit/test_fileinfo.py +204 -0
  50. anidb_client-1.0.0/tests/unit/test_filename_inference.py +193 -0
  51. anidb_client-1.0.0/tests/unit/test_http_timeouts.py +136 -0
  52. anidb_client-1.0.0/tests/unit/test_init_credentials.py +143 -0
  53. anidb_client-1.0.0/tests/unit/test_init_database.py +85 -0
  54. anidb_client-1.0.0/tests/unit/test_mapper.py +135 -0
  55. anidb_client-1.0.0/tests/unit/test_mylist.py +407 -0
  56. anidb_client-1.0.0/tests/unit/test_notfound_paths.py +208 -0
  57. anidb_client-1.0.0/tests/unit/test_objectlayer_fixture.py +130 -0
  58. anidb_client-1.0.0/tests/unit/test_package.py +86 -0
  59. anidb_client-1.0.0/tests/unit/test_ratelimit.py +262 -0
  60. anidb_client-1.0.0/tests/unit/test_release_tag.py +152 -0
  61. anidb_client-1.0.0/tests/unit/test_response_field_selection.py +171 -0
  62. anidb_client-1.0.0/tests/unit/test_responses.py +292 -0
  63. anidb_client-1.0.0/tests/unit/test_schema_snapshot.py +54 -0
  64. anidb_client-1.0.0/tests/unit/test_sql_url_credentials.py +169 -0
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CI invocation script for the drift detector agent.
5
+ *
6
+ * Determines the diff, passes it to Claude via the Agent SDK with the
7
+ * drift-detector agent instructions as system prompt, and exits with
8
+ * the appropriate code:
9
+ * 0 — no drift detected
10
+ * 1 — drift detected
11
+ * 2 — infrastructure error
12
+ */
13
+
14
+ import { execSync } from "node:child_process";
15
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { query } from "@anthropic-ai/claude-agent-sdk";
18
+
19
+ // --- Structured output schema ---
20
+
21
+ const driftSchema = {
22
+ type: "object",
23
+ properties: {
24
+ drift_detected: { type: "boolean" },
25
+ findings: {
26
+ type: "array",
27
+ items: {
28
+ type: "object",
29
+ properties: {
30
+ file: { type: "string" },
31
+ lines: { type: "string" },
32
+ affected_artifact: { type: "string" },
33
+ justification: { type: "string" },
34
+ suggestion: { type: "string" },
35
+ },
36
+ required: ["file", "affected_artifact", "justification", "suggestion"],
37
+ },
38
+ },
39
+ summary: { type: "string" },
40
+ },
41
+ required: ["drift_detected", "findings", "summary"],
42
+ };
43
+
44
+ // --- Helpers ---
45
+
46
+ function git(cmd) {
47
+ return execSync(`git ${cmd}`, { encoding: "utf-8" }).trim();
48
+ }
49
+
50
+ function parseArgs(argv) {
51
+ const args = { base: null, head: null };
52
+ for (let i = 2; i < argv.length; i++) {
53
+ if (argv[i] === "--base" && argv[i + 1]) {
54
+ args.base = argv[++i];
55
+ } else if (argv[i] === "--head" && argv[i + 1]) {
56
+ args.head = argv[++i];
57
+ }
58
+ }
59
+ return args;
60
+ }
61
+
62
+ function determineDiff(cliArgs) {
63
+ // 1. Explicit CLI refs
64
+ if (cliArgs.base && cliArgs.head) {
65
+ return git(`diff ${cliArgs.base}..${cliArgs.head}`);
66
+ }
67
+
68
+ // 2. GitLab MR pipeline — diff the MR base against current commit
69
+ const mrBase = process.env.CI_MERGE_REQUEST_DIFF_BASE_SHA;
70
+ const commitSha = process.env.CI_COMMIT_SHA;
71
+ if (mrBase && commitSha) {
72
+ return git(`diff ${mrBase}..${commitSha}`);
73
+ }
74
+
75
+ // 3. Branch pipeline — diff against merge base with default branch
76
+ const branch = process.env.CI_COMMIT_BRANCH;
77
+ const defaultBranch = process.env.CI_DEFAULT_BRANCH;
78
+ if (branch && defaultBranch) {
79
+ if (branch === defaultBranch) {
80
+ // On main: diff the latest commit only
81
+ return git("diff HEAD~1..HEAD");
82
+ }
83
+ // Feature branch: deepen history and fetch default branch to find merge base
84
+ try { git("fetch --unshallow"); } catch {}
85
+ git(`fetch origin ${defaultBranch}`);
86
+ const mergeBase = git(`merge-base origin/${defaultBranch} HEAD`);
87
+ return git(`diff ${mergeBase}..HEAD`);
88
+ }
89
+
90
+ // 4. Cannot determine — fail explicitly
91
+ console.error(
92
+ "Error: Cannot determine diff. Provide --base and --head, " +
93
+ "or run in a GitLab MR/branch pipeline.",
94
+ );
95
+ process.exit(2);
96
+ }
97
+
98
+ function formatFindings(result) {
99
+ if (!result.drift_detected || result.findings.length === 0) {
100
+ console.log("No drift detected.");
101
+ return;
102
+ }
103
+
104
+ for (const f of result.findings) {
105
+ const location = f.lines ? `${f.file}:${f.lines}` : f.file;
106
+ console.log(`DRIFT: ${location}`);
107
+ console.log(` Affected: ${f.affected_artifact}`);
108
+ console.log(` Reason: ${f.justification}`);
109
+ console.log(` Suggestion: ${f.suggestion}`);
110
+ console.log();
111
+ }
112
+
113
+ if (result.summary) {
114
+ console.log(result.summary);
115
+ }
116
+ }
117
+
118
+ // --- Main ---
119
+
120
+ async function main() {
121
+ const cliArgs = parseArgs(process.argv);
122
+ const diff = determineDiff(cliArgs);
123
+
124
+ if (!diff) {
125
+ console.log("Empty diff — nothing to check.");
126
+ process.exit(0);
127
+ }
128
+
129
+ // Read the agent instructions from the markdown file (strip frontmatter)
130
+ const agentPath = join(process.cwd(), ".claude", "agents", "drift-detector.md");
131
+ const agentRaw = readFileSync(agentPath, "utf-8");
132
+ const agentPrompt = agentRaw.replace(/^---[\s\S]*?---\n*/, "");
133
+
134
+ const prompt = [
135
+ "Analyze the following diff for drift against the project's specs, ADRs, tests, and navigation aids.",
136
+ "",
137
+ "```diff",
138
+ diff,
139
+ "```",
140
+ ].join("\n");
141
+
142
+ let structured = null;
143
+ let resultMessage = null;
144
+
145
+ for await (const message of query({
146
+ prompt,
147
+ options: {
148
+ systemPrompt: agentPrompt,
149
+ allowedTools: ["Read", "Glob", "Grep", "Bash"],
150
+ permissionMode: "acceptEdits",
151
+ settingSources: ["project"],
152
+ cwd: process.cwd(),
153
+ outputFormat: {
154
+ type: "json_schema",
155
+ schema: driftSchema,
156
+ },
157
+ },
158
+ })) {
159
+ if (message.type === "result") {
160
+ resultMessage = message;
161
+ structured = message.structured_output;
162
+ }
163
+ }
164
+
165
+ if (!resultMessage) {
166
+ console.error("Error: No result received from drift detector.");
167
+ process.exit(2);
168
+ }
169
+
170
+ // Try structured_output first, fall back to parsing result text
171
+ if (!structured && resultMessage.result) {
172
+ try {
173
+ structured = JSON.parse(resultMessage.result);
174
+ } catch {
175
+ console.error("Error: Failed to parse result as JSON.");
176
+ console.error(resultMessage.result);
177
+ process.exit(2);
178
+ }
179
+ }
180
+
181
+ if (!structured) {
182
+ console.error("Error: No output from drift detector.");
183
+ process.exit(2);
184
+ }
185
+
186
+ formatFindings(structured);
187
+ process.exit(structured.drift_detected ? 1 : 0);
188
+ }
189
+
190
+ main().catch((err) => {
191
+ console.error("Error: Drift detection failed.");
192
+ console.error(err.message || err);
193
+ if (err.stack) console.error(err.stack);
194
+ process.exit(2);
195
+ });
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """Validate ADR frontmatter and generate docs/decisions/INDEX.md."""
3
+
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+
10
+ DECISIONS_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "docs" / "decisions"
11
+ INDEX_PATH = DECISIONS_DIR / "INDEX.md"
12
+ FILENAME_RE = re.compile(r"^ADR-(\d{3})-[\w-]+\.md$")
13
+ VALID_STATUSES = {"draft", "accepted"}
14
+ REQUIRED_FIELDS = ("title", "description", "status", "tags")
15
+ REQUIRED_SECTIONS = ("Context", "Decision", "Consequences")
16
+
17
+
18
+ def parse_frontmatter(path: Path) -> dict | None:
19
+ """Extract YAML frontmatter from a file. Returns None if no frontmatter."""
20
+ text = path.read_text(encoding="utf-8")
21
+ if not text.startswith("---"):
22
+ return None
23
+ end = text.find("---", 3)
24
+ if end == -1:
25
+ return None
26
+ raw = text[3:end]
27
+ return yaml.safe_load(raw)
28
+
29
+
30
+ def validate_adrs(adr_files: list[Path]) -> tuple[list[dict], list[str], list[str]]:
31
+ """Validate all ADR files. Returns (parsed entries, errors, warnings)."""
32
+ errors: list[str] = []
33
+ warnings: list[str] = []
34
+ entries: list[dict] = []
35
+
36
+ for path in adr_files:
37
+ name = path.name
38
+ match = FILENAME_RE.match(name)
39
+ if not match:
40
+ errors.append(f"ERROR [{name}]: Filename doesn't match ADR-NNN-slug.md pattern")
41
+ continue
42
+
43
+ number = int(match.group(1))
44
+ fm = parse_frontmatter(path)
45
+
46
+ if fm is None:
47
+ errors.append(f"ERROR [{name}]: Missing YAML frontmatter entirely")
48
+ continue
49
+
50
+ file_errors = False
51
+ for field in REQUIRED_FIELDS:
52
+ if not fm.get(field):
53
+ errors.append(f"ERROR [{name}]: Missing required field: {field}")
54
+ file_errors = True
55
+
56
+ status = fm.get("status", "")
57
+ if status == "draft":
58
+ errors.append(f"ERROR [{name}]: status is 'draft' (signals incomplete work)")
59
+ file_errors = True
60
+ elif status and status not in VALID_STATUSES:
61
+ errors.append(f"ERROR [{name}]: Unrecognized status '{status}' (expected 'draft' or 'accepted')")
62
+ file_errors = True
63
+
64
+ # Check required body sections
65
+ text = path.read_text(encoding="utf-8")
66
+ body = text.split("---", 2)[-1] if text.startswith("---") else text
67
+ for section in REQUIRED_SECTIONS:
68
+ if not re.search(rf"^#{{1,6}}\s+{section}\b", body, re.MULTILINE):
69
+ errors.append(f"ERROR [{name}]: Missing required section: {section}")
70
+ file_errors = True
71
+
72
+ if not file_errors:
73
+ entries.append(
74
+ {
75
+ "number": number,
76
+ "filename": name,
77
+ "title": fm["title"],
78
+ "description": fm["description"],
79
+ "status": fm["status"],
80
+ "tags": ", ".join(fm.get("tags", [])),
81
+ }
82
+ )
83
+
84
+ return entries, errors, warnings
85
+
86
+
87
+ def generate_index(entries: list[dict]) -> str:
88
+ """Generate INDEX.md content from validated entries."""
89
+ entries.sort(key=lambda e: e["number"])
90
+
91
+ lines = [
92
+ "<!-- AUTO-GENERATED by .claude/skills/adr/scripts/generate_index.py — do not edit manually -->",
93
+ "",
94
+ "# Architecture Decision Records",
95
+ "",
96
+ "| Number | Title | Description | Status | Tags |",
97
+ "|--------|-------|-------------|--------|------|",
98
+ ]
99
+
100
+ for e in entries:
101
+ num = f"ADR-{e['number']:03d}"
102
+ link = f"[{num}]({e['filename']})"
103
+ lines.append(f"| {link} | {e['title']} | {e['description']} | {e['status']} | {e['tags']} |")
104
+
105
+ lines.append("")
106
+ return "\n".join(lines)
107
+
108
+
109
+ def main() -> int:
110
+ if not DECISIONS_DIR.is_dir():
111
+ print(f"ERROR: Directory not found: {DECISIONS_DIR}", file=sys.stderr)
112
+ return 1
113
+
114
+ adr_files = sorted(DECISIONS_DIR.glob("ADR-*.md"))
115
+ if not adr_files:
116
+ print("No ADR files found.", file=sys.stderr)
117
+ return 1
118
+
119
+ entries, errors, warnings = validate_adrs(adr_files)
120
+
121
+ for w in warnings:
122
+ print(w, file=sys.stderr)
123
+ for e in errors:
124
+ print(e, file=sys.stderr)
125
+
126
+ if errors:
127
+ print(f"\n{len(errors)} error(s) found. Fix them before INDEX.md can be generated.", file=sys.stderr)
128
+ return 1
129
+
130
+ index_content = generate_index(entries)
131
+
132
+ # --check verifies the committed index without writing, for CI. Done here rather
133
+ # than by regenerating and diffing with git, because the image this runs in is a
134
+ # Python toolchain and carries no git -- and adding one to compare two strings
135
+ # would be an unpinned apt package in the supply chain for no gain.
136
+ if "--check" in sys.argv[1:]:
137
+ committed = INDEX_PATH.read_text(encoding="utf-8") if INDEX_PATH.is_file() else None
138
+ if committed != index_content:
139
+ print(
140
+ f"ERROR: {INDEX_PATH} is out of date with the ADR frontmatter.\n"
141
+ f"Run `task adr-index` and commit the result.",
142
+ file=sys.stderr,
143
+ )
144
+ return 1
145
+ print(f"{INDEX_PATH} is up to date ({len(entries)} entries).")
146
+ return 0
147
+
148
+ INDEX_PATH.write_text(index_content, encoding="utf-8")
149
+ print(f"Generated {INDEX_PATH} with {len(entries)} entries.")
150
+ return 0
151
+
152
+
153
+ if __name__ == "__main__":
154
+ sys.exit(main())
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """Validate spec frontmatter and generate docs/specs/INDEX.md."""
3
+
4
+ import re
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+
10
+ SPECS_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "docs" / "specs"
11
+ INDEX_PATH = SPECS_DIR / "INDEX.md"
12
+ FILENAME_RE = re.compile(r"^SPEC-(\d{3})-[\w-]+\.md$")
13
+ VALID_STATUSES = {"draft", "accepted"}
14
+ REQUIRED_FIELDS = ("title", "description", "status", "tags")
15
+
16
+
17
+ def parse_frontmatter(path: Path) -> dict | None:
18
+ """Extract YAML frontmatter from a file. Returns None if no frontmatter."""
19
+ text = path.read_text(encoding="utf-8")
20
+ if not text.startswith("---"):
21
+ return None
22
+ end = text.find("---", 3)
23
+ if end == -1:
24
+ return None
25
+ raw = text[3:end]
26
+ return yaml.safe_load(raw)
27
+
28
+
29
+ def validate_specs(spec_files: list[Path]) -> tuple[list[dict], list[str], list[str]]:
30
+ """Validate all spec files. Returns (parsed entries, errors, warnings)."""
31
+ errors: list[str] = []
32
+ warnings: list[str] = []
33
+ entries: list[dict] = []
34
+
35
+ for path in spec_files:
36
+ name = path.name
37
+ match = FILENAME_RE.match(name)
38
+ if not match:
39
+ errors.append(f"ERROR [{name}]: Filename doesn't match SPEC-NNN-slug.md pattern")
40
+ continue
41
+
42
+ number = int(match.group(1))
43
+ fm = parse_frontmatter(path)
44
+
45
+ if fm is None:
46
+ errors.append(f"ERROR [{name}]: Missing YAML frontmatter entirely")
47
+ continue
48
+
49
+ file_errors = False
50
+ for field in REQUIRED_FIELDS:
51
+ if not fm.get(field):
52
+ errors.append(f"ERROR [{name}]: Missing required field: {field}")
53
+ file_errors = True
54
+
55
+ status = fm.get("status", "")
56
+ if status == "draft":
57
+ errors.append(f"ERROR [{name}]: status is 'draft' (signals incomplete work)")
58
+ file_errors = True
59
+ elif status and status not in VALID_STATUSES:
60
+ errors.append(f"ERROR [{name}]: Unrecognized status '{status}' (expected 'draft' or 'accepted')")
61
+ file_errors = True
62
+
63
+ if not file_errors:
64
+ entries.append(
65
+ {
66
+ "number": number,
67
+ "filename": name,
68
+ "title": fm["title"],
69
+ "description": fm["description"],
70
+ "status": fm["status"],
71
+ "tags": ", ".join(fm.get("tags", [])),
72
+ }
73
+ )
74
+
75
+ return entries, errors, warnings
76
+
77
+
78
+ def generate_index(entries: list[dict]) -> str:
79
+ """Generate INDEX.md content from validated entries."""
80
+ entries.sort(key=lambda e: e["number"])
81
+
82
+ lines = [
83
+ "<!-- AUTO-GENERATED by .claude/skills/spec/scripts/generate_index.py — do not edit manually -->",
84
+ "",
85
+ "# Specifications",
86
+ "",
87
+ "| Number | Title | Description | Status | Tags |",
88
+ "|--------|-------|-------------|--------|------|",
89
+ ]
90
+
91
+ for e in entries:
92
+ num = f"SPEC-{e['number']:03d}"
93
+ link = f"[{num}]({e['filename']})"
94
+ lines.append(f"| {link} | {e['title']} | {e['description']} | {e['status']} | {e['tags']} |")
95
+
96
+ lines.append("")
97
+ return "\n".join(lines)
98
+
99
+
100
+ def main() -> int:
101
+ if not SPECS_DIR.is_dir():
102
+ print(f"ERROR: Directory not found: {SPECS_DIR}", file=sys.stderr)
103
+ return 1
104
+
105
+ spec_files = sorted(SPECS_DIR.glob("SPEC-*.md"))
106
+ if not spec_files:
107
+ print("No spec files found.", file=sys.stderr)
108
+ return 1
109
+
110
+ entries, errors, warnings = validate_specs(spec_files)
111
+
112
+ for w in warnings:
113
+ print(w, file=sys.stderr)
114
+ for e in errors:
115
+ print(e, file=sys.stderr)
116
+
117
+ if errors:
118
+ print(f"\n{len(errors)} error(s) found. Fix them before INDEX.md can be generated.", file=sys.stderr)
119
+ return 1
120
+
121
+ index_content = generate_index(entries)
122
+
123
+ # --check verifies the committed index without writing, for CI. Done here rather
124
+ # than by regenerating and diffing with git, because the image this runs in is a
125
+ # Python toolchain and carries no git -- and adding one to compare two strings
126
+ # would be an unpinned apt package in the supply chain for no gain.
127
+ if "--check" in sys.argv[1:]:
128
+ committed = INDEX_PATH.read_text(encoding="utf-8") if INDEX_PATH.is_file() else None
129
+ if committed != index_content:
130
+ print(
131
+ f"ERROR: {INDEX_PATH} is out of date with the spec frontmatter.\n"
132
+ f"Run `task spec-index` and commit the result.",
133
+ file=sys.stderr,
134
+ )
135
+ return 1
136
+ print(f"{INDEX_PATH} is up to date ({len(entries)} entries).")
137
+ return 0
138
+
139
+ INDEX_PATH.write_text(index_content, encoding="utf-8")
140
+ print(f"Generated {INDEX_PATH} with {len(entries)} entries.")
141
+ return 0
142
+
143
+
144
+ if __name__ == "__main__":
145
+ sys.exit(main())
@@ -0,0 +1,67 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+
6
+ # Distribution / packaging
7
+ .Python
8
+ build/
9
+ develop-eggs/
10
+ dist/
11
+ downloads/
12
+ eggs/
13
+ .eggs/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ *.egg-info/
19
+ .installed.cfg
20
+ *.egg
21
+
22
+ # Virtualenvs / environments
23
+ .venv/
24
+ venv/
25
+ env/
26
+ ENV/
27
+
28
+ # uv
29
+ .uv-cache/
30
+
31
+ # Tooling caches
32
+ .ruff_cache/
33
+ .mypy_cache/
34
+ .pytest_cache/
35
+ .cache/
36
+ .tox/
37
+
38
+ # Test / coverage reports
39
+ htmlcov/
40
+ .coverage
41
+ .coverage.*
42
+ coverage.xml
43
+ report.xml
44
+ junit.xml
45
+ gl-code-quality-report.json
46
+
47
+ # Local cache written by the anime-titles / anime-list fetchers when a developer
48
+ # points them at the working tree instead of the system temp dir.
49
+ *.xml.gz
50
+
51
+ # Sphinx documentation
52
+ docs/_build/
53
+
54
+ # Secrets — never commit. The library reads credentials from a netrc file.
55
+ .netrc
56
+ *.netrc
57
+
58
+ # Local scratch databases
59
+ *.db
60
+ *.sqlite
61
+ *.sqlite3
62
+
63
+ # Editor / OS
64
+ .DS_Store
65
+ *.swp
66
+ .idea/
67
+ .vscode/