Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,9 @@ set -e
cd infrastructure
uv run pulumi down --yes --stack pocketsizefund/pocketsizefund/production
"""

[tasks."cli:datamanager:authorize"]
description = "Authorize the CLI with AWS credentials"
run = """
aws iam attach-user-policy --user-name {{arg(user-name="user-name")}} --policy-arn ${{pulumi stack output DATAMANAGER_API_ACCESS_POLICY_ARN}}
"""
4 changes: 1 addition & 3 deletions application/datamanager/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,4 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[dependency-groups]
dev = [
"behave>=1.2.6",
]
dev = ["behave>=1.2.6"]
138 changes: 138 additions & 0 deletions cli/datamanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import argparse
import json
from datetime import datetime, timedelta
from urllib.parse import urlparse
from zoneinfo import ZoneInfo

import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from loguru import logger


def sign_request(
method: str,
url: str,
data: dict | None = None,
region: str = "us-east-1",
) -> dict:
session = boto3.Session()
credentials = session.get_credentials()

request_payload = {
"method": method,
"url": url,
"headers": {
"Content-Type": "application/json",
"Host": urlparse(url).netloc,
},
}

if data:
request_payload["data"] = json.dumps(data)
request_payload["headers"]["Content-Type"] = "application/json"

request = AWSRequest(**request_payload)
SigV4Auth(credentials, "execute-api", region).add_auth(request)

return {
"method": method,
"url": url,
"headers": dict(request.headers),
"data": request.body,
}


def get_health(api_url: str, region: str) -> dict:
signed_request = sign_request(method="GET", url=f"{api_url}/health", region=region)
response = requests.request(**signed_request, timeout=10)
response.raise_for_status()
return response.json()


def get_equity_bars(api_url: str, start_date: str, end_date: str, region: str) -> dict:
url = f"{api_url}/equity-bars?start_date={start_date}&end_date={end_date}"
signed_request = sign_request(method="GET", url=url, region=region)
response = requests.request(**signed_request, timeout=30)
response.raise_for_status()
return response.json()


def get_metrics(api_url: str, region: str) -> dict:
signed_request = sign_request(method="GET", url=f"{api_url}/metrics", region=region)
response = requests.request(**signed_request, timeout=30)
response.raise_for_status()
return response.json()


def fetch_equity_bars(api_url: str, fetch_date: str, region: str) -> dict:
data = {"date": fetch_date}
signed_request = sign_request(
method="POST",
url=f"{api_url}/equity-bars/fetch",
data=data,
region=region,
)
response = requests.request(**signed_request, timeout=60)
response.raise_for_status()
return response.json()


def main() -> None:
parser = argparse.ArgumentParser(description="PocketSizeFund CLI Example")
parser.add_argument("--api-url", required=True, help="API Gateway URL")
parser.add_argument("--region", default="us-east-1", help="AWS region")
parser.add_argument(
"--command", choices=["health", "bars", "metrics", "fetch"], default="health"
)
parser.add_argument("--start-date", help="Start date for bars (YYYY-MM-DD)")
parser.add_argument("--end-date", help="End date for bars (YYYY-MM-DD)")
parser.add_argument("--fetch-date", help="Date to fetch bars for (YYYY-MM-DD)")

args = parser.parse_args()

eastern_timezone = ZoneInfo("America/New_York")

today = datetime.now(tz=eastern_timezone).date()
try:
if args.command == "health":
result = get_health(args.api_url, args.region)
logger.info(json.dumps(result, indent=2))

elif args.command == "metrics":
result = get_metrics(args.api_url, args.region)
logger.info(json.dumps(result, indent=2))

elif args.command == "bars":
if not args.start_date or not args.end_date:
# Default to last 7 days
end_date = today
start_date = end_date - timedelta(days=7)
start_date_str = start_date.isoformat()
end_date_str = end_date.isoformat()
else:
start_date_str = args.start_date
end_date_str = args.end_date

result = get_equity_bars(
args.api_url, start_date_str, end_date_str, args.region
)
records = result.get("records", [])
logger.info(f"found {len(records)} records")
if records:
logger.info(json.dumps(records[0], indent=2))

elif args.command == "fetch":
fetch_date = args.fetch_date or today.isoformat()
result = fetch_equity_bars(args.api_url, fetch_date, args.region)
logger.info(json.dumps(result, indent=2))

except requests.exceptions.HTTPError as e:
logger.error(f"http error response: {e.response.text}")
except Exception as e: # noqa: BLE001
logger.error(f"error: {e}")


if __name__ == "__main__":
main()
10 changes: 10 additions & 0 deletions cli/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[project]
name = "cli"
version = "0.1.0"
requires-python = "==3.12.10"
Comment thread
forstmeier marked this conversation as resolved.
dependencies = [
"requests>=2.31.0",
"loguru>=0.7.3",
"boto3>=1.38.23",
"botocore>=1.38.23",
]
41 changes: 41 additions & 0 deletions infrastructure/__main__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import tomllib
from pathlib import Path

import pulumi
from cluster import (
create_kubernetes_cluster,
create_kubernetes_provider,
update_kubernetes_cluster_access,
)
from environment_variables import create_environment_variables
from images import build_image
from ingress import (
create_alb_controller,
create_alb_controller_role,
create_api_access_policy,
create_api_gateway_with_auth,
create_service_ingress,
)
from keys import create_duckdb_user_access_key
from monitors import create_prometheus_scraper
from publishers_subscribers import (
Expand Down Expand Up @@ -42,6 +50,14 @@
root_user_arn=configuration.require_secret("AWS_EKS_IAM_ROOT_USER_ARN"),
)

alb_controller_role = create_alb_controller_role(kubernetes_cluster)

alb_controller = create_alb_controller(
kubernetes_provider=kubernetes_provider,
cluster=kubernetes_cluster,
alb_controller_role=alb_controller_role,
)

knative_serving_core = create_knative_serving_core(kubernetes_provider)

knative_eventing_core = create_knative_eventing_core(kubernetes_provider)
Expand Down Expand Up @@ -140,3 +156,28 @@
workspace_arn=configuration.require_secret("AWS_PROMETHEUS_WORKSPACE_ARN"),
cluster=kubernetes_cluster,
)

datamanager_ingress = create_service_ingress(
kubernetes_provider=kubernetes_provider,
service_name="datamanager",
cluster=kubernetes_cluster,
depends_on=[alb_controller, datamanager_service],
)

datamanager_alb_url = datamanager_ingress.status.load_balancer.ingress[
0
].hostname.apply(lambda hostname: f"http://{hostname}")

datamanager_api = create_api_gateway_with_auth(
service_name="datamanager",
target_url=datamanager_alb_url,
)
Comment thread
forstmeier marked this conversation as resolved.

datamanager_api_access_policy = create_api_access_policy(
api_gateway=datamanager_api,
service_name="datamanager",
)
Comment thread
forstmeier marked this conversation as resolved.

pulumi.export("DATAMANAGER_ALB_URL", datamanager_alb_url)
pulumi.export("DATAMANAGER_API_GATEWAY_URL", datamanager_api.api_endpoint)
pulumi.export("DATAMANAGER_API_ACCESS_POLICY_ARN", datamanager_api_access_policy.arn)
Loading