askamerica 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.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ .dev.vars
3
+ .wrangler/
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.4
2
+ Name: askamerica
3
+ Version: 0.1.0
4
+ Summary: Query US government data with a single line of Python
5
+ Project-URL: Homepage, https://askamerica.ai
6
+ Project-URL: Documentation, https://askamerica.ai/docs
7
+ Project-URL: Issues, https://github.com/askamerica/askamerica-python/issues
8
+ Author-email: AskAmerica <support@askamerica.ai>
9
+ License: MIT
10
+ Keywords: data,government,open-data,sql
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Requires-Python: >=3.8
23
+ Requires-Dist: duckdb>=0.9.0
24
+ Requires-Dist: requests>=2.28.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # AskAmerica
28
+
29
+ Query US government data with a single line of Python.
30
+
31
+ ```python
32
+ pip install askamerica
33
+ ```
34
+
35
+ ```python
36
+ import askamerica
37
+
38
+ askamerica.configure(api_key="aa_free_...")
39
+ df = askamerica.query("SELECT * FROM sec.filings WHERE year = 2024 LIMIT 100")
40
+ ```
41
+
42
+ Sign up for a free API key at [askamerica.ai](https://askamerica.ai).
@@ -0,0 +1,16 @@
1
+ # AskAmerica
2
+
3
+ Query US government data with a single line of Python.
4
+
5
+ ```python
6
+ pip install askamerica
7
+ ```
8
+
9
+ ```python
10
+ import askamerica
11
+
12
+ askamerica.configure(api_key="aa_free_...")
13
+ df = askamerica.query("SELECT * FROM sec.filings WHERE year = 2024 LIMIT 100")
14
+ ```
15
+
16
+ Sign up for a free API key at [askamerica.ai](https://askamerica.ai).
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,24 @@
1
+ from .client import query
2
+ from .auth import login
3
+ from .quota import get_quota
4
+ from .config import get_api_key
5
+ from .exceptions import AskAmericaError, AuthError, QuotaExceededError, QueryError
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = [
9
+ "query",
10
+ "login",
11
+ "get_quota",
12
+ "get_api_key",
13
+ "AskAmericaError",
14
+ "AuthError",
15
+ "QuotaExceededError",
16
+ "QueryError",
17
+ ]
18
+
19
+
20
+ def configure(api_key: str) -> None:
21
+ from .config import save_config, load_config
22
+ config = load_config()
23
+ config["api_key"] = api_key
24
+ save_config(config)
@@ -0,0 +1,29 @@
1
+ import getpass
2
+ import requests
3
+ from typing import Optional
4
+ from .config import API_BASE_URL, save_config
5
+ from .exceptions import AuthError
6
+
7
+
8
+ def login(email: Optional[str] = None) -> str:
9
+ if email is None:
10
+ email = input("Email: ").strip()
11
+
12
+ r = requests.post(f"{API_BASE_URL}/v1/auth/request-otp", json={"email": email})
13
+ if not r.ok:
14
+ raise AuthError(f"Failed to send OTP: {r.text}")
15
+
16
+ print(f"A 6-digit code has been sent to {email}.")
17
+ code = getpass.getpass("Enter code: ").strip()
18
+
19
+ r = requests.post(f"{API_BASE_URL}/v1/auth/verify-otp", json={"email": email, "code": code})
20
+ if not r.ok:
21
+ data = r.json()
22
+ raise AuthError(data.get("error", "verification failed"))
23
+
24
+ data = r.json()
25
+ api_key = data["api_key"]
26
+ save_config({"api_key": api_key, "email": email, "tier": data["tier"]})
27
+ print(f"Logged in. API key saved to ~/.askamerica/config.json")
28
+ print(f"Tier: {data['tier']} — {data['quota_gb']} GB/month")
29
+ return api_key
@@ -0,0 +1,72 @@
1
+ import sys
2
+ import argparse
3
+ from .auth import login
4
+ from .quota import get_quota
5
+ from .config import get_api_key
6
+ from .exceptions import AuthError
7
+
8
+
9
+ def cmd_login(args: argparse.Namespace) -> None:
10
+ login(email=getattr(args, "email", None))
11
+
12
+
13
+ def cmd_quota(args: argparse.Namespace) -> None:
14
+ key = get_api_key()
15
+ if not key:
16
+ print("Not logged in. Run: askamerica login")
17
+ sys.exit(1)
18
+ try:
19
+ quota = get_quota(key)
20
+ used_gb = quota["used_bytes"] / (1024 ** 3)
21
+ limit_gb = quota["limit_bytes"] / (1024 ** 3)
22
+ remaining_gb = quota["remaining_bytes"] / (1024 ** 3)
23
+ pct = (quota["used_bytes"] / quota["limit_bytes"]) * 100
24
+ print(f"Period: {quota['period']}")
25
+ print(f"Used: {used_gb:.3f} GB of {limit_gb:.0f} GB ({pct:.1f}%)")
26
+ print(f"Remaining: {remaining_gb:.3f} GB")
27
+ if quota["remaining_bytes"] < quota["limit_bytes"] * 0.2:
28
+ print(f"Upgrade: {quota['upgrade_url']}")
29
+ except AuthError as e:
30
+ print(f"Error: {e}")
31
+ sys.exit(1)
32
+
33
+
34
+ def cmd_whoami(args: argparse.Namespace) -> None:
35
+ from .config import load_config
36
+ config = load_config()
37
+ if not config:
38
+ print("Not logged in. Run: askamerica login")
39
+ sys.exit(1)
40
+ print(f"Email: {config.get('email', 'unknown')}")
41
+ print(f"Tier: {config.get('tier', 'free')}")
42
+ key = config.get("api_key", "")
43
+ print(f"Key: {key[:12]}...{key[-4:] if len(key) > 16 else ''}")
44
+
45
+
46
+ def main() -> None:
47
+ parser = argparse.ArgumentParser(
48
+ prog="askamerica",
49
+ description="AskAmerica — query US government data",
50
+ )
51
+ sub = parser.add_subparsers(dest="command")
52
+
53
+ p_login = sub.add_parser("login", help="Authenticate and get an API key")
54
+ p_login.add_argument("--email", help="Email address (optional, prompted if omitted)")
55
+
56
+ sub.add_parser("quota", help="Show current quota usage")
57
+ sub.add_parser("whoami", help="Show current login info")
58
+
59
+ args = parser.parse_args()
60
+
61
+ if args.command == "login":
62
+ cmd_login(args)
63
+ elif args.command == "quota":
64
+ cmd_quota(args)
65
+ elif args.command == "whoami":
66
+ cmd_whoami(args)
67
+ else:
68
+ parser.print_help()
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,97 @@
1
+ import time
2
+ import uuid
3
+ import duckdb
4
+ from typing import Any, Optional
5
+
6
+ from .config import get_api_key
7
+ from .exceptions import AuthError, QueryError
8
+ from .quota import check_quota, report_usage
9
+
10
+ ICEBERG_CATALOG_URL = "https://api.askamerica.ai/v1/catalog"
11
+
12
+
13
+ def _get_connection(api_key: str) -> duckdb.DuckDBPyConnection:
14
+ con = duckdb.connect()
15
+ con.execute("INSTALL iceberg; LOAD iceberg;")
16
+ con.execute("INSTALL httpfs; LOAD httpfs;")
17
+ con.execute(f"""
18
+ CREATE SECRET askamerica_r2 (
19
+ TYPE S3,
20
+ PROVIDER CREDENTIAL_CHAIN,
21
+ ENDPOINT 'api.askamerica.ai/v1/catalog/credentials',
22
+ EXTRA_HTTP_HEADERS MAP {{
23
+ 'X-API-Key': '{api_key}'
24
+ }}
25
+ )
26
+ """)
27
+ return con
28
+
29
+
30
+ def query(
31
+ sql: str,
32
+ api_key: Optional[str] = None,
33
+ return_type: str = "df",
34
+ ) -> Any:
35
+ key = api_key or get_api_key()
36
+ if not key:
37
+ raise AuthError("No API key configured. Run: askamerica login")
38
+
39
+ quota = check_quota(key)
40
+
41
+ query_id = str(uuid.uuid4())
42
+ start_ms = time.time() * 1000
43
+
44
+ try:
45
+ con = duckdb.connect()
46
+ con.execute("INSTALL httpfs; LOAD httpfs;")
47
+
48
+ result = con.execute(sql)
49
+
50
+ duration_ms = int(time.time() * 1000 - start_ms)
51
+
52
+ if return_type == "df":
53
+ df = result.df()
54
+ row_count = len(df)
55
+ actual_bytes = df.memory_usage(deep=True).sum()
56
+ elif return_type == "arrow":
57
+ df = result.arrow()
58
+ row_count = df.num_rows
59
+ actual_bytes = df.nbytes
60
+ else:
61
+ raise QueryError(f"Unknown return_type: {return_type}. Use 'df' or 'arrow'.")
62
+
63
+ report_usage(
64
+ query_id=query_id,
65
+ table=_extract_table(sql),
66
+ planned_bytes=actual_bytes,
67
+ actual_bytes=actual_bytes,
68
+ row_count=row_count,
69
+ duration_ms=duration_ms,
70
+ query_text=sql,
71
+ api_key=key,
72
+ )
73
+
74
+ _print_quota_reminder(quota, actual_bytes)
75
+ return df
76
+
77
+ except duckdb.Error as e:
78
+ raise QueryError(str(e)) from e
79
+
80
+
81
+ def _extract_table(sql: str) -> str:
82
+ import re
83
+ match = re.search(r'\bFROM\s+([\w.]+)', sql, re.IGNORECASE)
84
+ return match.group(1) if match else "unknown"
85
+
86
+
87
+ def _print_quota_reminder(quota: dict, used_bytes: int) -> None:
88
+ remaining = quota["remaining_bytes"] - used_bytes
89
+ limit = quota["limit_bytes"]
90
+ pct_used = ((limit - remaining) / limit) * 100
91
+ remaining_gb = remaining / (1024 ** 3)
92
+ if pct_used >= 80:
93
+ print(
94
+ f"[askamerica] {pct_used:.0f}% of monthly quota used. "
95
+ f"{remaining_gb:.2f} GB remaining. "
96
+ f"Upgrade at https://askamerica.ai/upgrade"
97
+ )
@@ -0,0 +1,24 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ API_BASE_URL = "https://api.askamerica.ai"
7
+ CONFIG_PATH = Path.home() / ".askamerica" / "config.json"
8
+
9
+
10
+ def load_config() -> dict:
11
+ if CONFIG_PATH.exists():
12
+ with open(CONFIG_PATH) as f:
13
+ return json.load(f)
14
+ return {}
15
+
16
+
17
+ def save_config(config: dict) -> None:
18
+ CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
19
+ with open(CONFIG_PATH, "w") as f:
20
+ json.dump(config, f, indent=2)
21
+
22
+
23
+ def get_api_key() -> Optional[str]:
24
+ return os.environ.get("ASKAMERICA_API_KEY") or load_config().get("api_key")
@@ -0,0 +1,20 @@
1
+ class AskAmericaError(Exception):
2
+ pass
3
+
4
+
5
+ class AuthError(AskAmericaError):
6
+ pass
7
+
8
+
9
+ class QuotaExceededError(AskAmericaError):
10
+ def __init__(self, remaining_bytes: int, period: str):
11
+ self.remaining_bytes = remaining_bytes
12
+ self.period = period
13
+ super().__init__(
14
+ f"Monthly quota exceeded for {period}. "
15
+ f"Upgrade at https://askamerica.ai/upgrade"
16
+ )
17
+
18
+
19
+ class QueryError(AskAmericaError):
20
+ pass
@@ -0,0 +1,72 @@
1
+ import time
2
+ import requests
3
+ from typing import Optional
4
+ from .config import API_BASE_URL, get_api_key
5
+ from .exceptions import AuthError, QuotaExceededError
6
+
7
+ _cache: dict = {}
8
+ _cache_ttl = 300 # 5 minutes
9
+
10
+
11
+ def get_quota(api_key: Optional[str] = None) -> dict:
12
+ key = api_key or get_api_key()
13
+ if not key:
14
+ raise AuthError("No API key. Run: askamerica login")
15
+
16
+ now = time.time()
17
+ if _cache.get("expires_at", 0) > now:
18
+ return _cache["data"]
19
+
20
+ r = requests.get(f"{API_BASE_URL}/v1/quota", headers={"X-API-Key": key})
21
+ if r.status_code == 401:
22
+ raise AuthError("Invalid API key. Run: askamerica login")
23
+ r.raise_for_status()
24
+
25
+ data = r.json()
26
+ _cache["data"] = data
27
+ _cache["expires_at"] = now + _cache_ttl
28
+ return data
29
+
30
+
31
+ def check_quota(api_key: Optional[str] = None) -> dict:
32
+ quota = get_quota(api_key)
33
+ if quota["remaining_bytes"] <= 0:
34
+ raise QuotaExceededError(
35
+ remaining_bytes=quota["remaining_bytes"],
36
+ period=quota["period"],
37
+ )
38
+ return quota
39
+
40
+
41
+ def report_usage(
42
+ query_id: str,
43
+ table: str,
44
+ planned_bytes: int,
45
+ actual_bytes: int,
46
+ row_count: int,
47
+ duration_ms: int,
48
+ query_text: str,
49
+ api_key: Optional[str] = None,
50
+ ) -> None:
51
+ key = api_key or get_api_key()
52
+ if not key:
53
+ return
54
+ try:
55
+ requests.post(
56
+ f"{API_BASE_URL}/v1/metering/usage",
57
+ headers={"X-API-Key": key},
58
+ json={
59
+ "query_id": query_id,
60
+ "table": table,
61
+ "planned_bytes": planned_bytes,
62
+ "actual_bytes": actual_bytes,
63
+ "row_count": row_count,
64
+ "duration_ms": duration_ms,
65
+ "query_text": query_text,
66
+ },
67
+ timeout=5,
68
+ )
69
+ # invalidate quota cache after reporting
70
+ _cache.clear()
71
+ except Exception:
72
+ pass # metering is best-effort, never block the user
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "askamerica"
7
+ version = "0.1.0"
8
+ description = "Query US government data with a single line of Python"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ authors = [{ name = "AskAmerica", email = "support@askamerica.ai" }]
13
+ keywords = ["government", "data", "sql", "open-data"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Science/Research",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.8",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Topic :: Scientific/Engineering :: Information Analysis",
26
+ ]
27
+ dependencies = [
28
+ "requests>=2.28.0",
29
+ "duckdb>=0.9.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://askamerica.ai"
34
+ Documentation = "https://askamerica.ai/docs"
35
+ Issues = "https://github.com/askamerica/askamerica-python/issues"
36
+
37
+ [project.scripts]
38
+ askamerica = "askamerica.cli:main"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["askamerica"]