Skip to content

Commit 96265d5

Browse files
committed
work in progress
1 parent b6c4d58 commit 96265d5

20 files changed

Lines changed: 3068 additions & 0 deletions

src/query_farm_flight_server/__init__.py

Whitespace-only changes.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import datetime
2+
import json
3+
import re
4+
from typing import Any
5+
6+
import boto3
7+
import click
8+
from prettytable import PrettyTable
9+
from pydantic import BaseModel, Field
10+
11+
from . import auth_manager as am
12+
from . import auth_manager_dynamodb
13+
14+
15+
def validate_email(ctx: Any, param: Any, value: str) -> str:
16+
email_regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
17+
if not re.match(email_regex, value):
18+
raise click.BadParameter("Invalid email address.")
19+
return value
20+
21+
22+
def build(
23+
*,
24+
auth_manager: auth_manager_dynamodb.AuthManagerDynamoDB[am.AccountType, am.TokenType],
25+
) -> Any:
26+
@click.group()
27+
def cli() -> None:
28+
pass
29+
30+
@click.command(help="Create a token for an account")
31+
@click.option("--account-id", type=str, required=True, help="Account ID")
32+
def create_token(account_id: str) -> None:
33+
assert auth_manager.account_by_id(account_id)
34+
token = auth_manager.create_token(account_id=account_id)
35+
auth_manager.upsert_token(token)
36+
print(f"Added token {token.token}")
37+
38+
@click.command(help="List accounts")
39+
def list_accounts() -> None:
40+
accounts = auth_manager.list_accounts()
41+
42+
t = PrettyTable()
43+
t.field_names = ["Account ID", "Name", "Email"]
44+
for account in accounts:
45+
t.add_row([account.account_id, account.name, account.email])
46+
print(t)
47+
48+
@click.command(help="List tokens for an account")
49+
@click.option("--account-id", type=str, required=True, help="Account ID")
50+
def list_tokens(account_id: str) -> None:
51+
tokens = auth_manager.list_tokens_for_account_id(account_id)
52+
53+
t = PrettyTable()
54+
t.field_names = ["Token"]
55+
for token in tokens:
56+
t.add_row([token.token])
57+
print(t)
58+
59+
@click.command(help="API usage by account")
60+
@click.option("--current-month", is_flag=True, help="Only show the current month")
61+
def usage_by_account(current_month: bool) -> None:
62+
dynamodb = boto3.resource("dynamodb", region_name=auth_manager._aws_region)
63+
rate_limit_table = dynamodb.Table(auth_manager._rate_limit_table_name)
64+
65+
current_yyyymm = datetime.datetime.now().strftime("%Y%m")
66+
67+
class RateLimitCount(BaseModel):
68+
account_id: str
69+
yyyymm: int
70+
request_count: int = Field(default=0)
71+
request_seconds: float
72+
transferred_bytes: int
73+
74+
if current_month:
75+
records = rate_limit_table.scan(
76+
FilterExpression="yyyymm = :yyyymm",
77+
ExpressionAttributeValues={":yyyymm": current_yyyymm},
78+
)["Items"]
79+
else:
80+
records = rate_limit_table.scan()["Items"]
81+
82+
usage = [RateLimitCount(**i) for i in records]
83+
84+
t = PrettyTable()
85+
t.field_names = [
86+
"Account ID",
87+
"YYYY-MMM",
88+
"Request Count",
89+
"Request Seconds",
90+
"Transferred Bytes",
91+
]
92+
for u in usage:
93+
t.add_row(
94+
[
95+
u.account_id,
96+
u.yyyymm,
97+
u.request_count,
98+
u.request_seconds,
99+
u.transferred_bytes,
100+
]
101+
)
102+
print(t)
103+
104+
@click.command(help="Get an account")
105+
@click.option("--account-id", required=True, type=str)
106+
def get_account(account_id: str) -> None:
107+
account = auth_manager.account_by_id(account_id)
108+
print(account.model_dump_json(indent=1))
109+
110+
@click.command(help="Update an existing account")
111+
@click.option("--account-id", required=True, type=str)
112+
@click.option("--data", required=True, type=click.Path(exists=True))
113+
def update_account(account_id: str, data: str) -> None:
114+
with open(data) as reader:
115+
account_data = json.load(reader)
116+
account = auth_manager.create_account(**account_data)
117+
account.account_id = account_id
118+
auth_manager.upsert_account(account)
119+
print(f"Updated account {account_id}")
120+
121+
cli.add_command(list_accounts)
122+
cli.add_command(get_account)
123+
cli.add_command(update_account)
124+
cli.add_command(list_tokens)
125+
cli.add_command(create_token)
126+
cli.add_command(usage_by_account)
127+
return cli
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import uuid
2+
from datetime import UTC, datetime
3+
from decimal import Decimal
4+
5+
from pydantic import BaseModel, ConfigDict, Field
6+
7+
8+
class AccountToken(BaseModel):
9+
token: str = Field(default_factory=lambda: uuid.uuid4().hex)
10+
account_id: str
11+
last_used: datetime | None = Field(default=None)
12+
disabled: bool | None = Field(default=False)
13+
created: datetime = Field(default_factory=lambda: datetime.now(UTC))
14+
15+
16+
class Account(BaseModel):
17+
model_config = ConfigDict(arbitrary_types_allowed=True)
18+
19+
account_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
20+
email: str
21+
name: str
22+
monthly_request_limit: int = Field(default=-1)
23+
monthly_transfer_limit: float = Field(default=-1)
24+
monthly_request_seconds_limit: Decimal = Field(default=Decimal(-1))
25+
26+
disabled: bool | None = Field(default=False)
27+
created: datetime = Field(default_factory=lambda: datetime.now(UTC))
28+
expiration: datetime | None = Field(default=None)
29+
30+
31+
class TokenDisabled(Exception):
32+
pass
33+
34+
35+
class TokenUnknown(Exception):
36+
pass
37+
38+
39+
class AccountDisabled(Exception):
40+
pass
41+
42+
43+
class AccountUnknown(Exception):
44+
pass
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any, TypeVar
3+
4+
import structlog
5+
6+
from . import auth
7+
8+
log = structlog.get_logger()
9+
10+
T = TypeVar("T")
11+
12+
AccountType = TypeVar("AccountType", bound=auth.Account)
13+
TokenType = TypeVar("TokenType", bound=auth.AccountToken)
14+
15+
16+
# This is your virtual base class
17+
class AuthManager[AccountType: auth.Account, TokenType: auth.AccountToken](ABC):
18+
@abstractmethod
19+
def allow_anonymous_access(self) -> bool:
20+
"""Return True if anonymous access is allowed."""
21+
pass
22+
23+
@abstractmethod
24+
def create_account(self, **kwargs: Any) -> AccountType:
25+
pass
26+
27+
@abstractmethod
28+
def create_token(self, **kwargs: Any) -> TokenType:
29+
pass
30+
31+
@abstractmethod
32+
def data_for_token(self, token: str) -> TokenType:
33+
pass
34+
35+
@abstractmethod
36+
def account_by_id(self, account_id: str) -> AccountType:
37+
pass
38+
39+
@abstractmethod
40+
def account_ids_for_email_address(self, email: str) -> list[str]:
41+
pass
42+
43+
@abstractmethod
44+
def list_accounts(self) -> list[AccountType]:
45+
pass
46+
47+
@abstractmethod
48+
def list_tokens_for_account_id(self, account_id: str) -> list[TokenType]:
49+
pass
50+
51+
@abstractmethod
52+
def upsert_token(self, token: TokenType) -> None:
53+
pass
54+
55+
@abstractmethod
56+
def upsert_account(self, account: AccountType) -> None:
57+
pass

0 commit comments

Comments
 (0)