find-life 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,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: find-life
3
+ Version: 0.1.0
4
+ Summary: Taxonomic name search.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.3.3
8
+ Requires-Dist: httpx>=0.28.1
File without changes
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "find-life"
3
+ version = "0.1.0"
4
+ description = "Taxonomic name search."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "click>=8.3.3",
9
+ "httpx>=0.28.1",
10
+ ]
11
+
12
+ [project.scripts]
13
+ find-life = "find_life.cli:cli"
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=61.0"] # Use a common build backend
20
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import click
4
+
5
+ from find_life.init import run_init
6
+ from find_life.search import run_search
7
+
8
+
9
+ @click.group()
10
+ def cli() -> None:
11
+ """find-life: taxonomic name search."""
12
+
13
+
14
+ @cli.command()
15
+ @click.argument("name")
16
+ @click.option(
17
+ "--scientific",
18
+ "mode",
19
+ flag_value="scientific",
20
+ help="Search scientific names only.",
21
+ )
22
+ @click.option(
23
+ "--common", "mode", flag_value="common", help="Search vernacular names only."
24
+ )
25
+ @click.option(
26
+ "-f",
27
+ "--format",
28
+ "output_fmt",
29
+ type=click.Choice(["text", "table", "json"], case_sensitive=False),
30
+ default="text",
31
+ show_default=True,
32
+ help="Output format.",
33
+ )
34
+ def search(name: str, mode: str | None, output_fmt: str) -> None:
35
+ """Search taxonomic units by scientific or common name."""
36
+ run_search(name, mode, output_fmt)
37
+
38
+
39
+ @cli.command()
40
+ def init() -> None:
41
+ """Re-run setup: reset API key, change mode, or reconfigure."""
42
+ run_init()
43
+
44
+
45
+ # TODO coming soon!
46
+ # @cli.command()
47
+ # def suggest() -> None:
48
+ # """Open a pre-filled issue with query context."""
49
+ # click.echo("suggest")
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import secrets
5
+ import socket
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ import click
10
+ import httpx
11
+
12
+ BASE_URL = "https://find-life.org"
13
+ CONFIG_DIR = Path.home() / ".find-life"
14
+ CONFIG_FILE = CONFIG_DIR / "config.json"
15
+
16
+
17
+ def load_config() -> dict:
18
+ CONFIG_DIR.mkdir(exist_ok=True)
19
+ if not CONFIG_FILE.exists():
20
+ return {}
21
+ return json.loads(CONFIG_FILE.read_text())
22
+
23
+
24
+ def save_config(token: str) -> None:
25
+ CONFIG_DIR.mkdir(exist_ok=True)
26
+ CONFIG_FILE.write_text(json.dumps({"find-life-token": token}, indent=2))
27
+
28
+
29
+ def make_token_name() -> str:
30
+ host = socket.gethostname().split(".")[0]
31
+ date = datetime.now().strftime("%Y%m%d")
32
+ suffix = secrets.token_hex(4)
33
+ return f"{host}-{date}-{suffix}"
34
+
35
+
36
+ def provision_token(token: str) -> str:
37
+ response = httpx.post(f"{BASE_URL}/api", json={"token": token}, timeout=10)
38
+ response.raise_for_status()
39
+ result = response.json()["token"]
40
+ click.echo(f"New token created, saved to {CONFIG_FILE}")
41
+ return result
42
+
43
+
44
+ def ensure_token() -> str:
45
+ config = load_config()
46
+ token = config.get("find-life-token", "")
47
+ today = datetime.now().strftime("%Y%m%d")
48
+ if token and today in token:
49
+ return token
50
+ token = provision_token(make_token_name())
51
+ save_config(token)
52
+ return token
53
+
54
+
55
+ def run_init() -> None:
56
+ try:
57
+ token = provision_token(make_token_name())
58
+ save_config(token)
59
+ except httpx.HTTPStatusError as e:
60
+ click.echo(f"Request failed: {e.response.status_code} {e.response.text}")
61
+ except httpx.RequestError as e:
62
+ click.echo(f"Connection failed: {e}")
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import json as json_mod
4
+ import shutil
5
+ import textwrap
6
+
7
+ import click
8
+ import httpx
9
+
10
+ from find_life.init import BASE_URL, ensure_token
11
+
12
+ _GRAY, _RESET, _BOLD = "\033[37m", "\033[0m", "\033[1m"
13
+
14
+
15
+ def _print_paleo(paleo: dict) -> None:
16
+ first, last = paleo.get("firstAppearance"), paleo.get("lastAppearance")
17
+ if first and last:
18
+ click.echo(f"\n{_BOLD}First appeared:{_RESET} {first} – {last} Ma")
19
+
20
+
21
+ def _print_wikipedia(wikipedia: dict) -> None:
22
+ if wikipedia.get("extract"):
23
+ term_w = shutil.get_terminal_size().columns
24
+ wrapped = textwrap.fill(
25
+ wikipedia["extract"],
26
+ width=term_w,
27
+ initial_indent=f"\n{_BOLD}Wikipedia:{_RESET} ",
28
+ subsequent_indent=" ",
29
+ )
30
+ click.echo(wrapped)
31
+ if wikipedia.get("source"):
32
+ click.echo(f"{_BOLD}Source:{_RESET} {wikipedia['source']}")
33
+
34
+
35
+ def _print_text(
36
+ taxonomy: dict, wikipedia: dict | None = None, paleo: dict | None = None
37
+ ) -> None:
38
+ parents = list(reversed(taxonomy.get("parents", [])))
39
+ subject = {"rank": taxonomy["rank"], "name": taxonomy["name"]}
40
+ children = taxonomy.get("children", [])
41
+
42
+ click.echo(f"\n{_BOLD}Taxonomy:{_RESET}")
43
+
44
+ for i, row in enumerate(parents):
45
+ indent = " " * i
46
+ branch = "" if i == 0 else "└─ "
47
+ click.echo(f"{_GRAY}{indent}{branch}{row['name']} ({row['rank']}){_RESET}")
48
+
49
+ subject_indent = " " * len(parents)
50
+ subject_prefix = f"{subject_indent}└─ " if parents else ""
51
+ click.echo(
52
+ f"{_BOLD}{subject_prefix}▶ {subject['name']} ({subject['rank']}){_RESET}"
53
+ )
54
+
55
+ child_indent = " " * (len(parents) + 1)
56
+ for i, row in enumerate(children):
57
+ branch = "└─" if i == len(children) - 1 else "├─"
58
+ click.echo(f"{child_indent}{branch} {row['name']} ({row['rank']})")
59
+
60
+ if paleo:
61
+ _print_paleo(paleo)
62
+ if wikipedia:
63
+ _print_wikipedia(wikipedia)
64
+
65
+
66
+ def _print_table(
67
+ taxonomy: dict, wikipedia: dict | None = None, paleo: dict | None = None
68
+ ) -> None:
69
+ rows: list[tuple[str, str, str]] = [
70
+ ("", "Name", taxonomy["name"]),
71
+ ("", "Rank", taxonomy["rank"]),
72
+ ]
73
+ for p in reversed(taxonomy.get("parents", [])):
74
+ rows.append(("Parents", p["rank"], p["name"]))
75
+ for c in taxonomy.get("children", []):
76
+ rows.append(("Children", c["rank"], c["name"]))
77
+ if paleo:
78
+ first, last = paleo.get("firstAppearance"), paleo.get("lastAppearance")
79
+ if first:
80
+ rows.append(("Paleo", "First appearance", f"{first} Ma"))
81
+ if last:
82
+ rows.append(("", "Last appearance", f"{last} Ma"))
83
+ if wikipedia:
84
+ if wikipedia.get("extract"):
85
+ rows.append(("Wikipedia", "Extract", wikipedia["extract"]))
86
+ if wikipedia.get("source"):
87
+ rows.append(("Wikipedia", "Source", wikipedia["source"]))
88
+
89
+ sec_w = max(len(r[0]) for r in rows)
90
+ field_w = max(len(r[1]) for r in rows)
91
+ term_w = shutil.get_terminal_size().columns
92
+ value_w = max(term_w - sec_w - field_w - 6, 40)
93
+
94
+ click.echo(f"{'Section':<{sec_w}} {'Field':<{field_w}} Value")
95
+ click.echo("-" * min(shutil.get_terminal_size().columns, 80))
96
+
97
+ prev_section = None
98
+ for section, field, value in rows:
99
+ sec_label = section if section != prev_section else ""
100
+ wrapped = textwrap.wrap(value, width=value_w) or [""]
101
+ click.echo(f"{sec_label:<{sec_w}} {field:<{field_w}} {wrapped[0]}")
102
+ indent = " " * (sec_w + field_w + 6)
103
+ for line in wrapped[1:]:
104
+ click.echo(f"{indent}{line}")
105
+ prev_section = section
106
+
107
+
108
+ def run_search(name: str, mode: str | None, output_fmt: str) -> None:
109
+ try:
110
+ token = ensure_token()
111
+ except httpx.RequestError as e:
112
+ click.echo(f"Connection failed: {e}")
113
+ return
114
+
115
+ def fetch(action: str) -> dict | None:
116
+ response = httpx.get(
117
+ f"{BASE_URL}/api",
118
+ params={"action": action, "q": name},
119
+ headers={"x-find-life-token": token},
120
+ )
121
+ response.raise_for_status()
122
+ data = response.json()
123
+ return data if data.get("id") else None
124
+
125
+ try:
126
+ data = fetch(mode) if mode else (fetch("scientific") or fetch("common"))
127
+ if not data:
128
+ click.echo(f"No results found for {name!r}")
129
+ return
130
+ taxonomy = data.get("taxonomy")
131
+ if not taxonomy:
132
+ click.echo(f"ID: {data['id']}")
133
+ return
134
+ if output_fmt == "json":
135
+ click.echo(
136
+ json_mod.dumps(
137
+ {
138
+ "taxonomy": taxonomy,
139
+ "paleo": data.get("paleo"),
140
+ "wikipedia": data.get("wikipedia"),
141
+ },
142
+ indent=2,
143
+ )
144
+ )
145
+ elif output_fmt == "table":
146
+ _print_table(taxonomy, data.get("wikipedia"), data.get("paleo"))
147
+ else:
148
+ _print_text(taxonomy, data.get("wikipedia"), data.get("paleo"))
149
+ except httpx.HTTPStatusError as e:
150
+ click.echo(f"Request failed: {e.response.status_code} {e.response.text}")
151
+ except httpx.RequestError as e:
152
+ click.echo(f"Connection failed: {e}")
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: find-life
3
+ Version: 0.1.0
4
+ Summary: Taxonomic name search.
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.3.3
8
+ Requires-Dist: httpx>=0.28.1
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/find_life/__init__.py
4
+ src/find_life/cli.py
5
+ src/find_life/init.py
6
+ src/find_life/search.py
7
+ src/find_life.egg-info/PKG-INFO
8
+ src/find_life.egg-info/SOURCES.txt
9
+ src/find_life.egg-info/dependency_links.txt
10
+ src/find_life.egg-info/entry_points.txt
11
+ src/find_life.egg-info/requires.txt
12
+ src/find_life.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ find-life = find_life.cli:cli
@@ -0,0 +1,2 @@
1
+ click>=8.3.3
2
+ httpx>=0.28.1
@@ -0,0 +1 @@
1
+ find_life