mad-search 0.1.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.
@@ -0,0 +1,13 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Search config files (contain API keys)
13
+ search.md
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The mad-search authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: mad-search
3
+ Version: 0.1.0
4
+ Summary: Search OpenAlex for academic articles using YAML-frontmatter markdown files
5
+ Project-URL: Homepage, https://codeberg.org/divinemadman/mad-search
6
+ Project-URL: Repository, https://codeberg.org/divinemadman/mad-search.git
7
+ Project-URL: Issues, https://codeberg.org/divinemadman/mad-search/issues
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.13
10
+ Requires-Dist: pyalex>=0.21
11
+ Requires-Dist: pyyaml>=6.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # mad-search
15
+
16
+ [![Codeberg](https://img.shields.io/badge/hosted-Codeberg-2185D0?logo=codeberg)](https://codeberg.org/divinemadman/mad-search)
17
+
18
+ Search OpenAlex for academic articles using YAML-frontmatter markdown files.
19
+
20
+ ## Requirements
21
+
22
+ - Python 3.13+
23
+ - A free [OpenAlex API key](https://openalex.org/settings/api)
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ # run directly with uv
29
+ uv run mad-search path/to/search.md
30
+
31
+ # or install globally
32
+ uv tool install mad-search
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ Create a markdown file with YAML frontmatter:
38
+
39
+ ```markdown
40
+ ---
41
+ api_key: your_key_here
42
+ query: "(machine learning AND climate) OR large language models"
43
+ journals:
44
+ - Nature
45
+ - Science
46
+ year_from: 2020
47
+ year_to: 2025
48
+ ---
49
+ ```
50
+
51
+ Run it:
52
+
53
+ ```bash
54
+ uv run mad-search search.md
55
+ ```
56
+
57
+ Results are appended to the same file, overwriting any previous results.
58
+
59
+ ## Features
60
+
61
+ - **Boolean search** — `AND`, `OR`, `NOT` with parentheses, passed directly to OpenAlex's Elasticsearch backend
62
+ - **Journal filter** — names auto-resolved to ISSNs via the OpenAlex Sources endpoint
63
+ - **Year range** — optional `year_from` / `year_to` constraints
64
+ - **Result re-ranking** — results scored by title match (40%), abstract match (20%), and concept overlap (40%) after retrieval
65
+ - **Structured output** — summary table plus detail sections with DOI, citation count, FWCI, concepts (top 5, score ≥ 0.3), and abstract
66
+ - **Idempotent** — re-running replaces the previous results section
67
+
68
+ ## Output
69
+
70
+ Results are appended as a markdown table with detail sections:
71
+
72
+ ```markdown
73
+ ## Results — 5 paper(s) found
74
+
75
+ | # | Title | DOI | Journal | Year | Citations | Authors |
76
+ |---|---|---|---|---|---|---|
77
+
78
+ ---
79
+
80
+ ### 1. Paper Title
81
+
82
+ - **DOI:** `10.xxx/...`
83
+ - **Authors:** ...
84
+ - **Journal:** ...
85
+ - **Citations:** 124 | FWCI: 3.2
86
+ - **Concepts:** Climate Science (0.92), Machine Learning (0.87)
87
+ - **Abstract:** ...
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,79 @@
1
+ # mad-search
2
+
3
+ [![Codeberg](https://img.shields.io/badge/hosted-Codeberg-2185D0?logo=codeberg)](https://codeberg.org/divinemadman/mad-search)
4
+
5
+ Search OpenAlex for academic articles using YAML-frontmatter markdown files.
6
+
7
+ ## Requirements
8
+
9
+ - Python 3.13+
10
+ - A free [OpenAlex API key](https://openalex.org/settings/api)
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ # run directly with uv
16
+ uv run mad-search path/to/search.md
17
+
18
+ # or install globally
19
+ uv tool install mad-search
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Create a markdown file with YAML frontmatter:
25
+
26
+ ```markdown
27
+ ---
28
+ api_key: your_key_here
29
+ query: "(machine learning AND climate) OR large language models"
30
+ journals:
31
+ - Nature
32
+ - Science
33
+ year_from: 2020
34
+ year_to: 2025
35
+ ---
36
+ ```
37
+
38
+ Run it:
39
+
40
+ ```bash
41
+ uv run mad-search search.md
42
+ ```
43
+
44
+ Results are appended to the same file, overwriting any previous results.
45
+
46
+ ## Features
47
+
48
+ - **Boolean search** — `AND`, `OR`, `NOT` with parentheses, passed directly to OpenAlex's Elasticsearch backend
49
+ - **Journal filter** — names auto-resolved to ISSNs via the OpenAlex Sources endpoint
50
+ - **Year range** — optional `year_from` / `year_to` constraints
51
+ - **Result re-ranking** — results scored by title match (40%), abstract match (20%), and concept overlap (40%) after retrieval
52
+ - **Structured output** — summary table plus detail sections with DOI, citation count, FWCI, concepts (top 5, score ≥ 0.3), and abstract
53
+ - **Idempotent** — re-running replaces the previous results section
54
+
55
+ ## Output
56
+
57
+ Results are appended as a markdown table with detail sections:
58
+
59
+ ```markdown
60
+ ## Results — 5 paper(s) found
61
+
62
+ | # | Title | DOI | Journal | Year | Citations | Authors |
63
+ |---|---|---|---|---|---|---|
64
+
65
+ ---
66
+
67
+ ### 1. Paper Title
68
+
69
+ - **DOI:** `10.xxx/...`
70
+ - **Authors:** ...
71
+ - **Journal:** ...
72
+ - **Citations:** 124 | FWCI: 3.2
73
+ - **Concepts:** Climate Science (0.92), Machine Learning (0.87)
74
+ - **Abstract:** ...
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "mad-search"
3
+ version = "0.1.0"
4
+ description = "Search OpenAlex for academic articles using YAML-frontmatter markdown files"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "pyalex>=0.21",
9
+ "pyyaml>=6.0",
10
+ ]
11
+
12
+ [project.urls]
13
+ Homepage = "https://codeberg.org/divinemadman/mad-search"
14
+ Repository = "https://codeberg.org/divinemadman/mad-search.git"
15
+ Issues = "https://codeberg.org/divinemadman/mad-search/issues"
16
+
17
+ [project.scripts]
18
+ mad-search = "mad_search.__main__:main"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/mad_search"]
@@ -0,0 +1,5 @@
1
+ from .models import SearchConfig, WorkResult
2
+ from .search import run_search
3
+ from .config import parse_frontmatter
4
+
5
+ __all__ = ["SearchConfig", "WorkResult", "run_search", "parse_frontmatter"]
@@ -0,0 +1,65 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ import pyalex
5
+
6
+ from .config import parse_frontmatter
7
+ from .journals import resolve_journals
8
+ from .search import run_search
9
+ from .formatter import format_results
10
+
11
+
12
+ def main() -> None:
13
+ if len(sys.argv) != 2:
14
+ print("Usage: mad-search <path/to/search.md>")
15
+ sys.exit(1)
16
+
17
+ filepath = Path(sys.argv[1])
18
+ if not filepath.exists():
19
+ print(f"Error: file not found: {filepath}")
20
+ sys.exit(1)
21
+
22
+ try:
23
+ config, body = parse_frontmatter(filepath)
24
+ except ValueError as e:
25
+ print(f"Error: {e}")
26
+ sys.exit(1)
27
+
28
+ pyalex.config.api_key = config.api_key
29
+
30
+ print(f"Query: {config.query}")
31
+ if config.journals:
32
+ print(f"Journals: {', '.join(config.journals)}")
33
+ if config.year_from or config.year_to:
34
+ print(f"Year range: {config.year_from or 'any'} \u2014 {config.year_to or 'any'}")
35
+
36
+ issns = resolve_journals(config.journals)
37
+ if config.journals and not issns:
38
+ print("Error: none of the specified journals could be resolved.")
39
+ sys.exit(1)
40
+ if issns:
41
+ print(f"Resolved ISSNs: {', '.join(issns)}")
42
+
43
+ print("Searching OpenAlex\u2026")
44
+
45
+ try:
46
+ results = run_search(config, issns)
47
+ except RuntimeError as e:
48
+ print(f"Error: {e}")
49
+ sys.exit(1)
50
+
51
+ print(f"Found {len(results)} result(s)")
52
+
53
+ output = format_results(results)
54
+
55
+ content = filepath.read_text(encoding="utf-8")
56
+
57
+ if "## Results" in content:
58
+ content = content[: content.index("## Results")].rstrip()
59
+
60
+ filepath.write_text(content + "\n\n" + output + "\n", encoding="utf-8")
61
+ print(f"Written to {filepath}")
62
+
63
+
64
+ if __name__ == "__main__":
65
+ main()
@@ -0,0 +1,49 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from .models import SearchConfig
7
+
8
+
9
+ def parse_frontmatter(path: Path) -> tuple[SearchConfig, str]:
10
+ content = path.read_text(encoding="utf-8")
11
+
12
+ match = re.match(r"^---\s*\n(.*?)\n\s*---\s*\n?(.*)", content, re.DOTALL)
13
+ if not match:
14
+ raise ValueError(
15
+ "No YAML frontmatter found. "
16
+ "File must start with `---\\n...\\n---`."
17
+ )
18
+
19
+ raw_yaml = match.group(1)
20
+ body = match.group(2)
21
+
22
+ try:
23
+ data = yaml.safe_load(raw_yaml)
24
+ except yaml.YAMLError as e:
25
+ raise ValueError(f"Invalid YAML frontmatter: {e}")
26
+
27
+ if not isinstance(data, dict):
28
+ raise ValueError("YAML frontmatter must be a mapping (key: value pairs)")
29
+
30
+ api_key = data.get("api_key")
31
+ if not api_key:
32
+ raise ValueError(
33
+ "`api_key` is required in frontmatter. "
34
+ "Get one at https://openalex.org/settings/api"
35
+ )
36
+
37
+ query = data.get("query")
38
+ if not query:
39
+ raise ValueError("`query` is required in frontmatter")
40
+
41
+ config = SearchConfig(
42
+ api_key=str(api_key),
43
+ query=str(query),
44
+ journals=data.get("journals", []) or [],
45
+ year_from=data.get("year_from"),
46
+ year_to=data.get("year_to"),
47
+ )
48
+
49
+ return config, body
@@ -0,0 +1,53 @@
1
+ from .models import WorkResult
2
+
3
+
4
+ def format_results(results: list[WorkResult]) -> str:
5
+ lines: list[str] = []
6
+
7
+ if not results:
8
+ lines.append("## Results")
9
+ lines.append("")
10
+ lines.append("*No results found.*")
11
+ return "\n".join(lines)
12
+
13
+ lines.append(f"## Results \u2014 {len(results)} paper(s) found")
14
+ lines.append("")
15
+
16
+ lines.append("| # | Title | DOI | Journal | Year | Citations | Authors |")
17
+ lines.append("|---|---|---|---|---|---|---|")
18
+ for i, r in enumerate(results, start=1):
19
+ authors_str = ", ".join(r.authors[:3])
20
+ if len(r.authors) > 3:
21
+ authors_str += " et al."
22
+ title = (r.title or "N/A")[:80]
23
+ doi = r.doi or "N/A"
24
+ journal = (r.journal or "N/A")[:40]
25
+ year = str(r.year or "N/A")
26
+ citations = f"{r.cited_by_count:,}" if r.cited_by_count is not None else "N/A"
27
+ lines.append(f"| {i} | {title} | {doi} | {journal} | {year} | {citations} | {authors_str} |")
28
+
29
+ lines.append("")
30
+ lines.append("---")
31
+ lines.append("")
32
+
33
+ for i, r in enumerate(results, start=1):
34
+ lines.append(f"### {i}. {r.title or 'Untitled'}")
35
+ lines.append("")
36
+ lines.append(f"- **DOI:** `{r.doi or 'N/A'}`")
37
+ lines.append(f"- **Authors:** {', '.join(r.authors) or 'N/A'}")
38
+ lines.append(f"- **Journal:** {r.journal or 'N/A'} ({r.year or 'N/A'})")
39
+ if r.openalex_id:
40
+ lines.append(f"- **OpenAlex:** [{r.openalex_id}]({r.openalex_id})")
41
+ if r.cited_by_count is not None:
42
+ fwci_str = f" | FWCI: {r.fwci:.1f}" if r.fwci is not None else ""
43
+ lines.append(f"- **Citations:** {r.cited_by_count:,}{fwci_str}")
44
+ if r.concepts:
45
+ concepts_str = ", ".join(f"{c['name']} ({c['score']:.2f})" for c in r.concepts)
46
+ lines.append(f"- **Concepts:** {concepts_str}")
47
+ if r.abstract:
48
+ abstract = r.abstract[:1000]
49
+ suffix = "\u2026" if len(r.abstract) > 1000 else ""
50
+ lines.append(f"- **Abstract:** {abstract}{suffix}")
51
+ lines.append("")
52
+
53
+ return "\n".join(lines)
@@ -0,0 +1,29 @@
1
+ from pyalex import Sources
2
+
3
+
4
+ def resolve_journals(names: list[str]) -> list[str]:
5
+ if not names:
6
+ return []
7
+
8
+ resolved: list[str] = []
9
+
10
+ for name in names:
11
+ try:
12
+ results = list(Sources().search(name).filter(type="journal").get(per_page=3))
13
+ except Exception as e:
14
+ print(f" \u26a0 Error looking up journal '{name}': {e}")
15
+ continue
16
+
17
+ if not results:
18
+ print(f" \u26a0 Could not resolve journal: '{name}'")
19
+ continue
20
+
21
+ source = results[0]
22
+ issn = source.get("issn_l") or (source.get("issn") or [None])[0]
23
+ if issn:
24
+ resolved.append(issn)
25
+ print(f" \u2713 {name} \u2192 {issn}")
26
+ else:
27
+ print(f" \u26a0 Journal '{name}' found but has no ISSN")
28
+
29
+ return resolved
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional
3
+
4
+
5
+ @dataclass
6
+ class SearchConfig:
7
+ api_key: str
8
+ query: str
9
+ journals: list[str] = field(default_factory=list)
10
+ year_from: Optional[int] = None
11
+ year_to: Optional[int] = None
12
+
13
+
14
+ @dataclass
15
+ class WorkResult:
16
+ doi: Optional[str]
17
+ title: Optional[str]
18
+ authors: list[str]
19
+ journal: Optional[str]
20
+ year: Optional[int]
21
+ openalex_id: Optional[str]
22
+ abstract: Optional[str]
23
+ concepts: list[dict] = field(default_factory=list)
24
+ cited_by_count: Optional[int] = None
25
+ fwci: Optional[float] = None
@@ -0,0 +1,58 @@
1
+ import re
2
+
3
+ from .models import WorkResult
4
+
5
+ STOP_WORDS = {"the", "a", "an", "of", "in", "on", "at", "to", "for", "and", "or", "not", "is", "by", "with",
6
+ "from", "as", "be", "this", "that", "it", "its"}
7
+
8
+
9
+ def _extract_terms(query: str) -> list[str]:
10
+ cleaned = re.sub(r"\b(?:AND|OR|NOT)\b", " ", query)
11
+ cleaned = re.sub(r"[()\"'']", " ", cleaned)
12
+ tokens = [t.lower() for t in cleaned.split()
13
+ if t.lower() not in STOP_WORDS and len(t) > 1]
14
+ return list(set(tokens))
15
+
16
+
17
+ def _title_score(title: str | None, terms: list[str]) -> float:
18
+ if not title or not terms:
19
+ return 0.0
20
+ t = title.lower()
21
+ return sum(1 for term in terms if term in t) / len(terms)
22
+
23
+
24
+ def _abstract_score(abstract: str | None, terms: list[str]) -> float:
25
+ if not abstract or not terms:
26
+ return 0.0
27
+ a = abstract.lower()
28
+ return sum(1 for term in terms if term in a) / len(terms)
29
+
30
+
31
+ def _concept_score(concepts: list[dict], terms: list[str]) -> float:
32
+ if not concepts or not terms:
33
+ return 0.0
34
+ total = 0.0
35
+ count = 0
36
+ for c in concepts:
37
+ name = c["name"].lower()
38
+ if any(term in name or name in term for term in terms):
39
+ total += c["score"]
40
+ count += 1
41
+ return total / count if count else 0.0
42
+
43
+
44
+ def rerank(results: list[WorkResult], query: str) -> list[WorkResult]:
45
+ terms = _extract_terms(query)
46
+ if not terms:
47
+ return results
48
+
49
+ scored: list[tuple[float, WorkResult]] = []
50
+ for r in results:
51
+ ts = _title_score(r.title, terms)
52
+ aps = _abstract_score(r.abstract, terms)
53
+ cs = _concept_score(r.concepts, terms)
54
+ combined = 0.4 * ts + 0.2 * aps + 0.4 * cs
55
+ scored.append((combined, r))
56
+
57
+ scored.sort(key=lambda x: x[0], reverse=True)
58
+ return [r for _, r in scored]
@@ -0,0 +1,131 @@
1
+ import re
2
+
3
+ from pyalex import Works
4
+
5
+ from .models import SearchConfig, WorkResult
6
+ from .rerank import rerank
7
+
8
+
9
+ def decode_abstract(inverted_index: dict | None) -> str | None:
10
+ if not inverted_index:
11
+ return None
12
+ max_pos = max(max(positions) for positions in inverted_index.values())
13
+ words = [""] * (max_pos + 1)
14
+ for word, positions in inverted_index.items():
15
+ for pos in positions:
16
+ words[pos] = word
17
+ return " ".join(words)
18
+
19
+
20
+ def _split_or_query(query: str) -> list[str]:
21
+ parts = []
22
+ depth = 0
23
+ start = 0
24
+ i = 0
25
+ while i < len(query):
26
+ if query[i] == "(":
27
+ depth += 1
28
+ elif query[i] == ")":
29
+ depth -= 1
30
+ elif depth == 0 and query[i:i+4] == " OR ":
31
+ parts.append(query[start:i].strip())
32
+ start = i + 4
33
+ i += 3
34
+ i += 1
35
+ parts.append(query[start:].strip())
36
+ return [p for p in parts if p]
37
+
38
+
39
+ def _strip_operators(s: str) -> str:
40
+ cleaned = re.sub(r"\b(?:AND|OR|NOT)\b", " ", s)
41
+ cleaned = re.sub(r"[()\"']", " ", cleaned)
42
+ return cleaned.strip()
43
+
44
+
45
+ def _parse_work(w: dict) -> WorkResult:
46
+ authors = [
47
+ a["author"]["display_name"]
48
+ for a in w.get("authorships", [])
49
+ if a.get("author")
50
+ ]
51
+ primary = w.get("primary_location") or {}
52
+ source = primary.get("source") or {}
53
+ journal = source.get("display_name")
54
+ abstract = decode_abstract(w.get("abstract_inverted_index"))
55
+ concepts_raw = w.get("concepts", [])
56
+ concepts = [
57
+ {"name": c["display_name"], "score": c["score"]}
58
+ for c in concepts_raw
59
+ if c.get("score", 0) >= 0.3
60
+ ][:5]
61
+ return WorkResult(
62
+ doi=w.get("doi"),
63
+ title=w.get("title"),
64
+ authors=authors,
65
+ journal=journal,
66
+ year=w.get("publication_year"),
67
+ openalex_id=w.get("id"),
68
+ abstract=abstract,
69
+ concepts=concepts,
70
+ cited_by_count=w.get("cited_by_count"),
71
+ fwci=w.get("fwci"),
72
+ )
73
+
74
+
75
+ def _search_branch(
76
+ search_q: str,
77
+ filters: dict,
78
+ seen_dois: set[str],
79
+ ) -> list[WorkResult]:
80
+ works = (
81
+ Works()
82
+ .search(search_q)
83
+ .filter(**filters)
84
+ .select([
85
+ "id", "doi", "title", "authorships",
86
+ "primary_location", "publication_year",
87
+ "abstract_inverted_index",
88
+ "concepts", "cited_by_count", "fwci",
89
+ ])
90
+ )
91
+
92
+ results: list[WorkResult] = []
93
+ for page in works.paginate(per_page=200, n_max=1000):
94
+ for w in page:
95
+ doi = w.get("doi")
96
+ if doi:
97
+ if doi in seen_dois:
98
+ continue
99
+ seen_dois.add(doi)
100
+ results.append(_parse_work(w))
101
+ return results
102
+
103
+
104
+ def run_search(config: SearchConfig, issns: list[str]) -> list[WorkResult]:
105
+ filters: dict = {}
106
+
107
+ if config.year_from is not None:
108
+ filters["from_publication_date"] = f"{config.year_from}-01-01"
109
+ if config.year_to is not None:
110
+ filters["to_publication_date"] = f"{config.year_to}-12-31"
111
+ if issns:
112
+ filters["primary_location"] = {"source": {"issn": "|".join(issns)}}
113
+
114
+ branches = _split_or_query(config.query)
115
+ if not branches:
116
+ branches = [config.query]
117
+
118
+ all_results: list[WorkResult] = []
119
+ seen_dois: set[str] = set()
120
+
121
+ try:
122
+ for branch in branches:
123
+ search_q = _strip_operators(branch)
124
+ if not search_q:
125
+ continue
126
+ all_results.extend(_search_branch(search_q, filters, seen_dois))
127
+ except Exception as e:
128
+ raise RuntimeError(f"OpenAlex API error: {e}")
129
+
130
+ all_results = rerank(all_results, config.query)
131
+ return all_results
@@ -0,0 +1,166 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.13"
4
+
5
+ [[package]]
6
+ name = "certifi"
7
+ version = "2026.6.17"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "charset-normalizer"
16
+ version = "3.4.7"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
19
+ wheels = [
20
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
21
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
22
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
23
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
24
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
25
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
26
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
27
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
28
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
29
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
30
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
31
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
32
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
33
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
34
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
35
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
36
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
37
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
38
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
39
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
40
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
41
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
42
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
43
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
44
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
45
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
46
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
47
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
48
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
49
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
50
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
51
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
52
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
53
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
54
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
55
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
56
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
57
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
58
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
59
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
60
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
61
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
62
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
63
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
64
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
65
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
66
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
67
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
68
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
69
+ ]
70
+
71
+ [[package]]
72
+ name = "idna"
73
+ version = "3.18"
74
+ source = { registry = "https://pypi.org/simple" }
75
+ sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
76
+ wheels = [
77
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
78
+ ]
79
+
80
+ [[package]]
81
+ name = "mad-search"
82
+ version = "0.1.0"
83
+ source = { editable = "." }
84
+ dependencies = [
85
+ { name = "pyalex" },
86
+ { name = "pyyaml" },
87
+ ]
88
+
89
+ [package.metadata]
90
+ requires-dist = [
91
+ { name = "pyalex", specifier = ">=0.21" },
92
+ { name = "pyyaml", specifier = ">=6.0" },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pyalex"
97
+ version = "0.21"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ dependencies = [
100
+ { name = "requests" },
101
+ { name = "urllib3" },
102
+ ]
103
+ sdist = { url = "https://files.pythonhosted.org/packages/c7/8a/5e270c3d6021a873667be7469fb95b00e0584593d57b7773ef0db6841cef/pyalex-0.21.tar.gz", hash = "sha256:39f470885187e0e411798d34163453361a3834c4dae53f0a18f272475b749741", size = 52593, upload-time = "2026-02-23T14:11:13.421Z" }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/c2/b6/714246176f5ad319dc1d06607a093570bc3d2c37de6d5583e106762883a5/pyalex-0.21-py3-none-any.whl", hash = "sha256:988c37eb31ee3d23176b431d37f7e18c37817317d976237abcc6f3c02f92396f", size = 15761, upload-time = "2026-02-23T14:11:12.523Z" },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "pyyaml"
110
+ version = "6.0.3"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
115
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
116
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
117
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
118
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
119
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
120
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
121
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
122
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
123
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
124
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
125
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
126
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
127
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
128
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
129
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
130
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
131
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
132
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
133
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
134
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
135
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
136
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
137
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
138
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
139
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
140
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
141
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
142
+ ]
143
+
144
+ [[package]]
145
+ name = "requests"
146
+ version = "2.34.2"
147
+ source = { registry = "https://pypi.org/simple" }
148
+ dependencies = [
149
+ { name = "certifi" },
150
+ { name = "charset-normalizer" },
151
+ { name = "idna" },
152
+ { name = "urllib3" },
153
+ ]
154
+ sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
155
+ wheels = [
156
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
157
+ ]
158
+
159
+ [[package]]
160
+ name = "urllib3"
161
+ version = "2.7.0"
162
+ source = { registry = "https://pypi.org/simple" }
163
+ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
164
+ wheels = [
165
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
166
+ ]