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.
- askamerica-0.1.0/._README.md +0 -0
- askamerica-0.1.0/._askamerica +0 -0
- askamerica-0.1.0/._dist +0 -0
- askamerica-0.1.0/._pyproject.toml +0 -0
- askamerica-0.1.0/.gitignore +3 -0
- askamerica-0.1.0/PKG-INFO +42 -0
- askamerica-0.1.0/README.md +16 -0
- askamerica-0.1.0/askamerica/.___init__.py +0 -0
- askamerica-0.1.0/askamerica/.___pycache__ +0 -0
- askamerica-0.1.0/askamerica/._auth.py +0 -0
- askamerica-0.1.0/askamerica/._cli.py +0 -0
- askamerica-0.1.0/askamerica/._client.py +0 -0
- askamerica-0.1.0/askamerica/._config.py +0 -0
- askamerica-0.1.0/askamerica/._exceptions.py +0 -0
- askamerica-0.1.0/askamerica/._quota.py +0 -0
- askamerica-0.1.0/askamerica/__init__.py +24 -0
- askamerica-0.1.0/askamerica/auth.py +29 -0
- askamerica-0.1.0/askamerica/cli.py +72 -0
- askamerica-0.1.0/askamerica/client.py +97 -0
- askamerica-0.1.0/askamerica/config.py +24 -0
- askamerica-0.1.0/askamerica/exceptions.py +20 -0
- askamerica-0.1.0/askamerica/quota.py +72 -0
- askamerica-0.1.0/pyproject.toml +41 -0
|
Binary file
|
|
Binary file
|
askamerica-0.1.0/._dist
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -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
|
|
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"]
|