akta-pro-cli 0.3.0__py3-none-any.whl

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,54 @@
1
+ """`akta-pro config` — view and change stored settings without re-entering the key."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from akta_pro_cli.client import DEFAULT_BASE_URL
10
+ from akta_pro_cli.config import credentials_path, load_credentials, save_credentials
11
+ from akta_pro_cli.console import err, out
12
+ from akta_pro_cli.runtime import EXIT_BAD_INPUT
13
+
14
+ config_app = typer.Typer(no_args_is_help=True, help="View or change stored settings (offline).")
15
+
16
+
17
+ def _mask(key: str | None) -> str:
18
+ if not key:
19
+ return "(not set)"
20
+ return f"{key[:5]}…{key[-4:]}" if len(key) > 12 else "…"
21
+
22
+
23
+ @config_app.command("show")
24
+ def show() -> None:
25
+ """Show the stored credentials/config (offline; API key masked)."""
26
+ creds = load_credentials()
27
+ base = creds.get("base_url")
28
+ out.print(f"config file : {credentials_path()}")
29
+ out.print(f"api key : {_mask(creds.get('api_key'))}")
30
+ out.print(f"base url : {base or f'(not set → default {DEFAULT_BASE_URL})'}")
31
+
32
+
33
+ @config_app.command("base-url")
34
+ def base_url(
35
+ url: Annotated[str | None, typer.Argument(help="New base URL. Omit and pass --reset to restore the default.")] = None,
36
+ reset: Annotated[bool, typer.Option("--reset", help="Reset to the default (api.akta.pro).")] = False,
37
+ ) -> None:
38
+ """Change or reset the stored base URL, keeping the stored API key."""
39
+ creds = load_credentials()
40
+ if reset:
41
+ creds.pop("base_url", None)
42
+ save_credentials(creds)
43
+ out.print(f"[green]✓[/] base url reset → default ({DEFAULT_BASE_URL})")
44
+ return
45
+ if not url:
46
+ err.print("[red]Provide a URL, or pass --reset.[/]")
47
+ raise typer.Exit(code=EXIT_BAD_INPUT)
48
+ creds["base_url"] = url
49
+ save_credentials(creds)
50
+ out.print(f"[green]✓[/] base url set → {url}")
51
+
52
+
53
+ def register(app: typer.Typer) -> None:
54
+ app.add_typer(config_app, name="config")
@@ -0,0 +1,46 @@
1
+ """`akta-pro industry` — resolve free-text industries to akta.pro codes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from rich.table import Table
9
+
10
+ from akta_pro_cli.options import JsonOpt, OutOpt
11
+ from akta_pro_cli.runtime import emit, fetch
12
+
13
+ app = typer.Typer(no_args_is_help=True, help="Industry search.")
14
+
15
+
16
+ def _industry_table(result: object) -> Table | None:
17
+ rows = result.get("data") if isinstance(result, dict) else None
18
+ if not rows:
19
+ return None
20
+ table = Table(title="Industry matches")
21
+ table.add_column("Code")
22
+ table.add_column("Industry", overflow="fold")
23
+ table.add_column("Similarity", justify="right")
24
+ for row in rows:
25
+ sim = row.get("similarity")
26
+ table.add_row(
27
+ str(row.get("code", "")),
28
+ str(row.get("industry_name", "")),
29
+ f"{sim:.3f}" if isinstance(sim, (int, float)) else str(sim or ""),
30
+ )
31
+ return table
32
+
33
+
34
+ @app.command("search")
35
+ def search(
36
+ ctx: typer.Context,
37
+ query: Annotated[str, typer.Argument(help="Free-text industry or topic, e.g. 'warehouse automation'.")],
38
+ json_out: JsonOpt = False,
39
+ output: OutOpt = None,
40
+ ) -> None:
41
+ """Resolve a free-text industry to akta.pro industry codes (free).
42
+
43
+ Use the returned `code` values as `--industry` in `akta-pro news`.
44
+ """
45
+ result = fetch(ctx.obj, "/industry/search", {"query": query})
46
+ emit(ctx.obj, result, json_out=json_out, output=output, renderer=_industry_table)
@@ -0,0 +1,232 @@
1
+ """`akta-pro news` — signals (list), detail (full bodies), and the type taxonomy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.console import Group
10
+ from rich.panel import Panel
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from akta_pro_cli.console import err
15
+ from akta_pro_cli.news_tags import NEWS_CATEGORIES, NEWS_TAGS
16
+ from akta_pro_cli.options import JsonOpt, OutOpt
17
+ from akta_pro_cli.runtime import EXIT_BAD_INPUT, emit, fetch
18
+
19
+ app = typer.Typer(no_args_is_help=True, help="News signals, article detail, and the type taxonomy.")
20
+
21
+ # Compact list fields (mirrors the MCP's news_signals shaping); `id` feeds `detail`.
22
+ _STRIPPED_FIELDS = ("id", "title", "ai_summary", "url", "published_date", "publisher")
23
+
24
+
25
+ class Sentiment(str, Enum):
26
+ positive = "positive"
27
+ negative = "negative"
28
+ neutral = "neutral"
29
+ all = "all"
30
+
31
+
32
+ class NewsScore(str, Enum):
33
+ high = "High"
34
+ medium = "Medium"
35
+ low = "Low"
36
+ all = "all"
37
+
38
+
39
+ def _shape(article: dict, *, full: bool) -> dict:
40
+ if full:
41
+ return {k: v for k, v in article.items() if k != "full_text"}
42
+ return {k: article.get(k) for k in _STRIPPED_FIELDS}
43
+
44
+
45
+ def _csv(values: list[str] | None) -> str | None:
46
+ """Join repeatable-option values into the comma-separated string the API
47
+ expects (or None when nothing was passed)."""
48
+ if not values:
49
+ return None
50
+ return ",".join(v.strip() for v in values if v.strip()) or None
51
+
52
+
53
+ def _signals_table(result: object) -> Table | None:
54
+ rows = result.get("data") if isinstance(result, dict) else None
55
+ if not rows:
56
+ return None
57
+ table = Table(title=f"News ({result.get('count', '?')} of {result.get('total', '?')})")
58
+ for col in ("Id", "Date", "Publisher", "Title", "AI summary", "URL"):
59
+ table.add_column(col, overflow="fold")
60
+ for row in rows:
61
+ table.add_row(
62
+ str(row.get("id", "") or ""),
63
+ str(row.get("published_date", "") or ""),
64
+ str(row.get("publisher", "") or ""),
65
+ str(row.get("title", "") or ""),
66
+ str(row.get("ai_summary", "") or ""),
67
+ str(row.get("url", "") or ""),
68
+ )
69
+ return table
70
+
71
+
72
+ @app.command("signals")
73
+ def signals(
74
+ ctx: typer.Context,
75
+ company: Annotated[str | None, typer.Option("--company", help="Company website or akta.pro UUID.")] = None,
76
+ industry: Annotated[str | None, typer.Option("--industry", help="Comma-separated industry codes (from `akta-pro industry search`) — preferred for sector/market topics.")] = None,
77
+ query: Annotated[str | None, typer.Option("--query", help="Open-ended topic, e.g. 'crude oil prices' (last resort; prefer --company/--industry).")] = None,
78
+ title: Annotated[str | None, typer.Option("--title", help="Search by text in the article title.")] = None,
79
+ start_date: Annotated[str | None, typer.Option("--start-date", help="Start of range, YYYY-MM-DD. Non-enterprise limited to ~6 months back.")] = None,
80
+ end_date: Annotated[str | None, typer.Option("--end-date", help="End of range, YYYY-MM-DD. Default today.")] = None,
81
+ type_codes: Annotated[list[str] | None, typer.Option("--type-code", "-t", help="News-type tag code(s) from `akta-pro news types`, e.g. SD01, CM03 (repeatable).")] = None,
82
+ sentiment: Annotated[Sentiment, typer.Option("--sentiment", help="Filter by article sentiment.")] = Sentiment.all,
83
+ news_score: Annotated[NewsScore, typer.Option("--news-score", help="Filter by relevance/quality tier.")] = NewsScore.all,
84
+ countries: Annotated[list[str] | None, typer.Option("--country", help="Filter by the event's country — ISO codes, e.g. USA, GBR (repeatable).")] = None,
85
+ blacklisted: Annotated[list[str] | None, typer.Option("--blacklist", help="Publisher domain(s) to exclude, e.g. example.com (repeatable).")] = None,
86
+ entity_person: Annotated[list[str] | None, typer.Option("--entity-person", help="Only articles mentioning these people, by name (repeatable).")] = None,
87
+ entity_location: Annotated[list[str] | None, typer.Option("--entity-location", help="Only articles mentioning these locations (repeatable).")] = None,
88
+ entity_product: Annotated[list[str] | None, typer.Option("--entity-product", help="Only articles mentioning these product names (repeatable).")] = None,
89
+ entity_event: Annotated[list[str] | None, typer.Option("--entity-event", help="Only articles mentioning these event names (repeatable).")] = None,
90
+ naics_codes: Annotated[list[str] | None, typer.Option("--naics", help="Filter by NAICS industry classification code(s) (repeatable).")] = None,
91
+ sic_codes: Annotated[list[str] | None, typer.Option("--sic", help="Filter by SIC industry classification code(s) (repeatable).")] = None,
92
+ iptc_codes: Annotated[list[str] | None, typer.Option("--iptc", help="Filter by IPTC media topic code(s) (repeatable).")] = None,
93
+ iab_codes: Annotated[list[str] | None, typer.Option("--iab", help="Filter by IAB content taxonomy code(s) (repeatable).")] = None,
94
+ group_articles: Annotated[bool, typer.Option("--group-articles", help="Group near-duplicate articles from the same event.")] = False,
95
+ limit: Annotated[int, typer.Option("-n", "--limit", min=1, max=1000, help="Max articles (max 1000).")] = 10,
96
+ offset: Annotated[int, typer.Option("--offset", min=0, help="Pagination offset.")] = 0,
97
+ full: Annotated[bool, typer.Option("--full", help="Return all per-article fields (industries, types, entities, mentions) except body text.")] = False,
98
+ json_out: JsonOpt = False,
99
+ output: OutOpt = None,
100
+ ) -> None:
101
+ """List news signals. Typically anchor the search with one of
102
+ --company/--industry/--query/--title (all optional).
103
+
104
+ The list is compact and never includes article bodies; each result carries an
105
+ `id` — pass those to `akta-pro news detail` for full text. Cost: 0.1/call +
106
+ 0.01/article. Filter by type with `-t` codes from `akta-pro news types`.
107
+ """
108
+ params = {
109
+ "company": company,
110
+ "industry": industry,
111
+ "query": query,
112
+ "title": title,
113
+ "start_date": start_date,
114
+ "end_date": end_date,
115
+ "sentiment_list": None if sentiment == Sentiment.all else sentiment.value,
116
+ "news_score_list": None if news_score == NewsScore.all else news_score.value,
117
+ "type_list": _csv(type_codes),
118
+ "countries": _csv(countries),
119
+ "blacklisted": _csv(blacklisted),
120
+ "entity_person_list": _csv(entity_person),
121
+ "entity_location_list": _csv(entity_location),
122
+ "entity_product_list": _csv(entity_product),
123
+ "entity_event_list": _csv(entity_event),
124
+ "naics_code_list": _csv(naics_codes),
125
+ "sic_code_list": _csv(sic_codes),
126
+ "iptc_code_list": _csv(iptc_codes),
127
+ "iab_code_list": _csv(iab_codes),
128
+ "group_articles": True if group_articles else None,
129
+ "limit": limit,
130
+ "offset": offset,
131
+ "full_text": False, # list stays compact; bodies via `news detail`
132
+ }
133
+ resp = fetch(ctx.obj, "/news", params)
134
+ articles = (resp.get("data") if isinstance(resp, dict) else None) or []
135
+ shaped = [_shape(a, full=full) for a in articles]
136
+ result = {
137
+ "total": resp.get("total") if isinstance(resp, dict) else None,
138
+ "count": len(shaped),
139
+ "credits_consumed": resp.get("credits_consumed") if isinstance(resp, dict) else None,
140
+ "data": shaped,
141
+ }
142
+ emit(ctx.obj, result, json_out=json_out, output=output, renderer=_signals_table)
143
+
144
+
145
+ def _detail_view(result: object) -> Group | None:
146
+ """Readable reader view for `news detail`: title + meta + article body.
147
+
148
+ Puts `full_text` front and centre (the whole point of `detail`); when the
149
+ body is empty, falls back to the AI summary and points at the source URL so
150
+ the output is still useful. Use --json for the full enrichment payload.
151
+ """
152
+ rows = result.get("data") if isinstance(result, dict) else None
153
+ if not rows:
154
+ return None
155
+ panels = []
156
+ for a in rows:
157
+ meta_bits = [str(a.get(k)) for k in ("publisher", "published_date") if a.get(k)]
158
+ meta = " · ".join(meta_bits)
159
+ url = a.get("url") or ""
160
+ body = (a.get("full_text") or "").strip()
161
+ if body:
162
+ content = Text(body)
163
+ else:
164
+ summary = (a.get("ai_summary") or "").strip()
165
+ content = Text()
166
+ content.append("Full article text is unavailable for this article.\n", style="yellow")
167
+ if summary:
168
+ content.append("\nAI summary:\n", style="bold")
169
+ content.append(summary + "\n")
170
+ if url:
171
+ content.append("\nRead the source: ", style="dim")
172
+ content.append(url, style="cyan underline")
173
+ header = Text(str(a.get("title") or f"#{a.get('id')}"), style="bold")
174
+ if meta:
175
+ header.append(f"\n{meta}", style="dim")
176
+ if body and url:
177
+ header.append(f"\n{url}", style="cyan")
178
+ panels.append(Panel(content, title=header, title_align="left",
179
+ subtitle=f"id {a.get('id')}", subtitle_align="right"))
180
+ return Group(*panels)
181
+
182
+
183
+ @app.command("detail")
184
+ def detail(
185
+ ctx: typer.Context,
186
+ news_ids: Annotated[list[int], typer.Argument(help="News id(s) from `akta-pro news signals` (max 10).")],
187
+ json_out: JsonOpt = False,
188
+ output: OutOpt = None,
189
+ ) -> None:
190
+ """Fetch full article bodies for specific news id(s). Cost: 0.1/call + 0.01/article.
191
+
192
+ In a terminal this renders a readable reader view — the article `full_text`
193
+ up top, falling back to the AI summary + source URL when a body isn't
194
+ available. Pass --json for the complete enrichment payload.
195
+ """
196
+ if not news_ids:
197
+ err.print("[red]Provide at least one news id[/] (from `akta-pro news signals`).")
198
+ raise typer.Exit(code=EXIT_BAD_INPUT)
199
+ ids_csv = ",".join(str(i) for i in news_ids[:10])
200
+ result = fetch(ctx.obj, "/news/by-id/", {"news_ids": ids_csv})
201
+ emit(ctx.obj, result, json_out=json_out, output=output, renderer=_detail_view)
202
+
203
+
204
+ def _types_table(_: object) -> Table:
205
+ table = Table(title=f"News types ({len(NEWS_TAGS)} tags)")
206
+ table.add_column("Code", style="bold cyan")
207
+ table.add_column("Category")
208
+ table.add_column("Type", overflow="fold")
209
+ for code, name in NEWS_TAGS:
210
+ table.add_row(code, NEWS_CATEGORIES.get(code[:2], ""), name)
211
+ return table
212
+
213
+
214
+ @app.command("types")
215
+ def types(
216
+ ctx: typer.Context,
217
+ json_out: JsonOpt = False,
218
+ output: OutOpt = None,
219
+ ) -> None:
220
+ """List the news-type tag codes for `--type-code` filtering (free, offline)."""
221
+ categories = []
222
+ for prefix, cat_name in NEWS_CATEGORIES.items():
223
+ tags = [{"code": c, "name": n} for c, n in NEWS_TAGS if c.startswith(prefix)]
224
+ if tags:
225
+ categories.append({"category": cat_name, "codes": tags})
226
+ result = {
227
+ "count": len(NEWS_TAGS),
228
+ "usage": "Pass matching code(s) to `akta-pro news signals --type-code CODE`.",
229
+ "categories": categories,
230
+ }
231
+ # No network/credits — flat table in a TTY, structured JSON otherwise.
232
+ emit(ctx.obj, result, json_out=json_out, output=output, renderer=_types_table)
@@ -0,0 +1,64 @@
1
+ """`akta-pro update` — check for a newer release and update the CLI in place."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ import subprocess
7
+ from typing import Annotated
8
+
9
+ import typer
10
+
11
+ from akta_pro_cli import __version__
12
+ from akta_pro_cli import update as _u
13
+ from akta_pro_cli.console import err, out
14
+ from akta_pro_cli.runtime import EXIT_API
15
+
16
+
17
+ def update(
18
+ check: Annotated[bool, typer.Option("--check", help="Only report whether an update exists; don't install.")] = False,
19
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip the confirmation prompt and update.")] = False,
20
+ ) -> None:
21
+ """Check for a newer release and, if found, reinstall it via pipx.
22
+
23
+ Discovers the latest version from PyPI and upgrades the installed package.
24
+ """
25
+ current = __version__
26
+ latest = _u.cached_latest(timeout=8.0, force=True)
27
+ if latest is None:
28
+ err.print(
29
+ f"[yellow]Couldn't reach PyPI to check for updates[/] (network?). "
30
+ f"You're on v{current}."
31
+ )
32
+ err.print(f"See {_u.PROJECT_URL}, or upgrade manually.")
33
+ raise typer.Exit(code=EXIT_API)
34
+
35
+ if not _u.is_newer(latest, current):
36
+ out.print(f"[green]✓ up to date[/] — v{current} is the latest.")
37
+ return
38
+
39
+ cmd = ["pipx", "upgrade", _u.PACKAGE]
40
+ out.print(f"Update available: [bold]v{current}[/] → [bold green]v{latest}[/]")
41
+
42
+ if check:
43
+ out.print("Run to update: " + " ".join(cmd))
44
+ return
45
+
46
+ if shutil.which("pipx") is None:
47
+ err.print("[yellow]pipx not found on PATH.[/] Update manually: " + " ".join(cmd))
48
+ raise typer.Exit(code=EXIT_API)
49
+
50
+ if not yes and not typer.confirm(f"Update to v{latest} now?", default=True):
51
+ out.print("Skipped. Run when ready: " + " ".join(cmd))
52
+ return
53
+
54
+ err.print(f"Updating to v{latest} via pipx…")
55
+ rc = subprocess.run(cmd).returncode
56
+ if rc == 0:
57
+ out.print(f"[green]✓ updated to v{latest}[/]")
58
+ else:
59
+ err.print(f"[red]pipx exited {rc}.[/] Try manually: " + " ".join(cmd))
60
+ raise typer.Exit(code=EXIT_API)
61
+
62
+
63
+ def register(app: typer.Typer) -> None:
64
+ app.command("update")(update)
akta_pro_cli/config.py ADDED
@@ -0,0 +1,71 @@
1
+ """Local credential + config storage for the akta.pro CLI.
2
+
3
+ Credentials live in a JSON file under the user's config dir
4
+ (`$XDG_CONFIG_HOME/akta-pro` or `~/.config/akta-pro` on Unix, `%APPDATA%\\akta-pro`
5
+ on Windows), written 0600. Only the API key path is used in v1.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import stat
13
+ from pathlib import Path
14
+
15
+ APP_NAME = "akta-pro"
16
+
17
+
18
+ def config_dir() -> Path:
19
+ if os.name == "nt":
20
+ base = os.environ.get("APPDATA") or str(Path.home() / "AppData" / "Roaming")
21
+ return Path(base) / APP_NAME
22
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
23
+ return Path(base) / APP_NAME
24
+
25
+
26
+ def credentials_path() -> Path:
27
+ return config_dir() / "credentials.json"
28
+
29
+
30
+ def load_credentials() -> dict:
31
+ path = credentials_path()
32
+ if not path.exists():
33
+ return {}
34
+ try:
35
+ data = json.loads(path.read_text())
36
+ return data if isinstance(data, dict) else {}
37
+ except (ValueError, OSError):
38
+ return {}
39
+
40
+
41
+ def save_credentials(data: dict) -> Path:
42
+ directory = config_dir()
43
+ directory.mkdir(parents=True, exist_ok=True)
44
+ try:
45
+ os.chmod(directory, 0o700)
46
+ except OSError:
47
+ pass # best-effort (e.g. Windows)
48
+ path = credentials_path()
49
+ path.write_text(json.dumps(data, indent=2))
50
+ try:
51
+ os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) # 0600
52
+ except OSError:
53
+ pass
54
+ return path
55
+
56
+
57
+ def clear_credentials() -> bool:
58
+ """Delete the stored credentials file. Returns True if a file was removed."""
59
+ path = credentials_path()
60
+ if path.exists():
61
+ path.unlink()
62
+ return True
63
+ return False
64
+
65
+
66
+ def stored_api_key() -> str | None:
67
+ return load_credentials().get("api_key")
68
+
69
+
70
+ def stored_base_url() -> str | None:
71
+ return load_credentials().get("base_url")
@@ -0,0 +1,10 @@
1
+ """Shared Rich consoles.
2
+
3
+ `out` writes machine/primary output to stdout; `err` writes human status,
4
+ credit lines, and errors to stderr — so `akta-pro ... --json | jq` stays clean.
5
+ """
6
+
7
+ from rich.console import Console
8
+
9
+ out = Console()
10
+ err = Console(stderr=True)
@@ -0,0 +1,102 @@
1
+ """akta.pro news-type taxonomy — a static snapshot of the published `news-tags.csv`.
2
+
3
+ Kept in the CLI so the package is fully independent of the MCP. The MCP has its
4
+ own copy (`akta_mcp/tools/news_tags.py`); keep both in sync if the CSV changes:
5
+ https://delightful-beach-02437b20f.7.azurestaticapps.net/news-tags.csv
6
+ """
7
+
8
+ # Leaf-tag category prefixes → human category names.
9
+ NEWS_CATEGORIES = {
10
+ "OT": "Others",
11
+ "SD": "Strategic Developments",
12
+ "CM": "Capital Markets & Transactions",
13
+ "LG": "Leadership & Governance",
14
+ "WT": "Workforce & Talent",
15
+ "FR": "Financial Results & Performance",
16
+ "RC": "Regulatory & Compliance",
17
+ "MM": "Macro & Market Environment",
18
+ "MD": "Market Dynamics",
19
+ "II": "Industry Disruption & Innovation",
20
+ "RK": "Risk & Crisis Events",
21
+ }
22
+
23
+ # (code, name) leaf tags. `news signals --type-code …` sends these as `type_list`.
24
+ NEWS_TAGS = [
25
+ ("OT01", "Others"),
26
+ ("SD01", "Product Launches & Enhancements"),
27
+ ("SD02", "Partnerships & Alliances"),
28
+ ("SD03", "Geographic Expansion"),
29
+ ("SD04", "Operational Milestones"),
30
+ ("SD05", "Market Share & Positioning"),
31
+ ("SD06", "Digital Transformation"),
32
+ ("SD07", "Brand & Marketing Initiatives"),
33
+ ("SD08", "Research, Development & Innovation"),
34
+ ("SD09", "Sustainability & ESG"),
35
+ ("SD10", "Pricing & Commercial Strategy"),
36
+ ("SD11", "Awards, Rankings & Recognitions"),
37
+ ("CM01", "Equity Fund-Raising"),
38
+ ("CM02", "Debt Financing"),
39
+ ("CM03", "Mergers & Acquisitions"),
40
+ ("CM04", "Divestitures & Asset Sales"),
41
+ ("CM05", "IPOs & Follow-on Offerings"),
42
+ ("CM06", "SPAC Deals & De-SPACs"),
43
+ ("CM07", "Secondary Transactions"),
44
+ ("CM08", "Shareholder Returns (Buybacks & Dividends)"),
45
+ ("CM09", "Valuation Events"),
46
+ ("CM10", "Deal Flow & Volume Statistics"),
47
+ ("CM11", "Public-to-Private/Take-Private"),
48
+ ("CM12", "Daily Stock Market News"),
49
+ ("LG01", "C-Suite Appointments & Departures"),
50
+ ("LG02", "Board Composition & Changes"),
51
+ ("LG03", "Succession Planning"),
52
+ ("LG04", "Organisational Restructuring"),
53
+ ("LG05", "Executive Commentary & Interviews"),
54
+ ("LG06", "Conflicts of Interest & Related-Party Transactions"),
55
+ ("WT01", "Strategic Hiring"),
56
+ ("WT02", "Workforce Reductions"),
57
+ ("WT03", "Compensation & Benefits"),
58
+ ("WT04", "Culture & Workplace Practices"),
59
+ ("WT05", "Talent-Market Dynamics"),
60
+ ("FR01", "Earnings Releases"),
61
+ ("FR02", "Revenue & Growth Metrics"),
62
+ ("FR03", "Profitability & Margins"),
63
+ ("FR04", "Cash Flow & Capital Expenditure"),
64
+ ("FR05", "Guidance & Forecast Revisions"),
65
+ ("FR06", "Credit-Rating Actions"),
66
+ ("FR07", "Sector Performance Dashboards"),
67
+ ("RC01", "Regulatory Approvals & Certifications"),
68
+ ("RC02", "Policy & Legislative Changes"),
69
+ ("RC03", "Legal Proceedings & Litigation"),
70
+ ("RC04", "Compliance & Reporting Updates"),
71
+ ("RC05", "Intellectual Property"),
72
+ ("RC06", "Antitrust & Competition"),
73
+ ("RC07", "Data Privacy & Cybersecurity Regulation"),
74
+ ("RC08", "Tax & Accounting Rules"),
75
+ ("MM01", "Economic Indicators"),
76
+ ("MM02", "Monetary Policy"),
77
+ ("MM03", "Fiscal Policy"),
78
+ ("MM04", "Trade, Tariffs & Sanctions"),
79
+ ("MM05", "Consumer Behaviour & Spending"),
80
+ ("MM06", "Labor-Market Statistics"),
81
+ ("MM07", "Sector Outlook & Forecasts"),
82
+ ("MD01", "Market Indices Performance"),
83
+ ("MD02", "Institutional Flows & Positioning"),
84
+ ("MD03", "Sector Rotation"),
85
+ ("MD04", "Volatility & Risk Appetite"),
86
+ ("MD05", "FX & Commodity Pricing"),
87
+ ("MD06", "Liquidity Conditions"),
88
+ ("MD07", "Investor Sentiment"),
89
+ ("II01", "Emerging Technologies"),
90
+ ("II02", "Technology Adoption & Disruption"),
91
+ ("II03", "Business-Model Evolution"),
92
+ ("II04", "Competitive Threats"),
93
+ ("II05", "Market-Structure Changes"),
94
+ ("RK01", "Geopolitical Risk"),
95
+ ("RK02", "Public-Health Crises"),
96
+ ("RK03", "Natural Disasters"),
97
+ ("RK04", "Operational Disruptions"),
98
+ ("RK05", "Cybersecurity Incidents"),
99
+ ("RK06", "Reputational Damage"),
100
+ ("RK07", "Financial Distress & Restructuring"),
101
+ ("RK08", "Supply-Chain Disruptions"),
102
+ ]
@@ -0,0 +1,21 @@
1
+ """Reusable Typer option/argument annotations shared across command modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ CompanyArg = Annotated[str, typer.Argument(help="Company website (e.g. 'canva.com') or akta.pro UUID.")]
11
+
12
+ JsonOpt = Annotated[bool, typer.Option("--json", help="Emit raw JSON to stdout (clean for pipes).")]
13
+
14
+ OutOpt = Annotated[
15
+ Path | None,
16
+ typer.Option("-o", "--output", help="Write the raw JSON/text payload to a file.", show_default=False),
17
+ ]
18
+
19
+ LimitOpt = Annotated[int, typer.Option("-n", "--limit", help="Max items to return.")]
20
+
21
+ OffsetOpt = Annotated[int, typer.Option("--offset", help="Pagination offset.")]