Skip to content

Commit 7daef2b

Browse files
committed
feat: fixup some code, add better docstrings, better example
1 parent 55d73f4 commit 7daef2b

19 files changed

Lines changed: 879 additions & 121 deletions

.github/workflows/build_and_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
steps:
1616
- name: Checkout code
17-
uses: actions/checkout@v3
17+
uses: actions/checkout@v4
1818

1919
- name: Initialize submodules
2020
env:

AGENTS.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# AGENTS.md
2+
3+
## Setup commands
4+
- Install deps: `uv sync --dev`
5+
- Build package: `uv build`
6+
- Run tests: `pytest`
7+
- Type check code: `pyright`
8+
- Format code: `ruff format`
9+
- Lint code: `ruff check`
10+
- Install git hooks: `pre-commit install`
11+
- Compile protobufs: `bash scripts/compile_proto.sh` (run from root)
12+
13+
## Code style
14+
- Use Python type hints throughout
15+
- Follow Black formatting (via ruff)
16+
- Async/await for I/O operations
17+
- Document public APIs with docstrings
18+
- Use functional patterns where possible
19+
- Implement proper error handling
20+
21+
## Testing instructions
22+
- Tests live in the `tests/` directory
23+
- Run `pytest` for full test suite
24+
- Run `pytest path/to/test.py` for specific tests
25+
- Run `pytest -k "test_name"` to run by pattern
26+
- Run `pytest --cov=src/athena_client` for coverage
27+
- Add tests for all new code
28+
- Mock external services in tests
29+
- Test both success and error cases
30+
31+
## PR instructions
32+
- Title format: [component] Description
33+
- Run `ruff check`, `pyright`, and `pytest` before committing
34+
- Keep PRs focused on a single change
35+
- Add tests for new functionality
36+
- Update documentation for API changes
37+
- Add proper type hints
38+
39+
## Dev tips
40+
- Use `uv` package manager instead of pip
41+
- Don't use `uv pip` commands, just the base `uv` commands
42+
- Run formatters before committing
43+
- Check generated code in `src/athena_client/generated/`
44+
- Add error handling at each pipeline stage
45+
- Use correlation IDs for request tracing
46+
47+
## Documentation
48+
- Docs are in `docs/`
49+
- Build docs by `cd docs && make clean && make html`

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ Make pipeline style invocation of the async interators such that we can
1010

1111
async read file -> async transform -> async classify -> async results
1212

13-
### Generate correlation IDs based on <something>
14-
Make do this with strategy pattern?
13+
### More async pipeline transformers
14+
Add additional pipeline transformers for:
15+
- Image format conversion
16+
- Metadata extraction
17+
- Error recovery and retry
18+
1519

1620

1721
## Development

examples/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Example scripts demonstrating usage of the Athena Client library.
2+
3+
This package contains practical examples and demonstration scripts showing
4+
how to integrate and use the Athena Client library in real applications.
5+
"""

examples/create_image.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Random image creation utilities."""
2+
3+
import io
4+
import random
5+
from collections.abc import AsyncIterator
6+
7+
from PIL import Image, ImageDraw
8+
9+
10+
def create_random_image(width: int = 640, height: int = 480) -> bytes:
11+
"""Create a random test image with random shapes and colors.
12+
13+
Args:
14+
width: Width of the test image in pixels
15+
height: Height of the test image in pixels
16+
17+
Returns:
18+
JPEG image bytes
19+
20+
"""
21+
# Create a new image with random background color
22+
image = Image.new(
23+
"RGB",
24+
(width, height),
25+
(
26+
random.randint(0, 255),
27+
random.randint(0, 255),
28+
random.randint(0, 255),
29+
),
30+
)
31+
32+
draw = ImageDraw.Draw(image)
33+
34+
# Draw some random shapes
35+
for _ in range(3):
36+
# Random coordinates
37+
x0 = random.randint(0, width - 1)
38+
y0 = random.randint(0, height - 1)
39+
x1 = random.randint(0, width - 1)
40+
y1 = random.randint(0, height - 1)
41+
42+
# Random color
43+
color = (
44+
random.randint(0, 255),
45+
random.randint(0, 255),
46+
random.randint(0, 255),
47+
)
48+
49+
draw.rectangle(
50+
[min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)], fill=color
51+
)
52+
53+
# Convert to JPEG bytes
54+
img_byte_arr = io.BytesIO()
55+
image.save(img_byte_arr, format="JPEG")
56+
return img_byte_arr.getvalue()
57+
58+
59+
async def iter_images(max_images: int | None = None) -> AsyncIterator[bytes]:
60+
"""Generate random test images.
61+
62+
Args:
63+
max_images: Maximum number of images to generate. If None, generates
64+
infinitely.
65+
66+
Yields:
67+
JPEG image bytes with random content
68+
69+
"""
70+
count = 0
71+
while max_images is None or count < max_images:
72+
yield create_random_image()
73+
count += 1

examples/example.py

Lines changed: 80 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,87 +2,18 @@
22
"""Example script that uses the athena client."""
33

44
import asyncio
5-
import io
65
import logging
76
import os
8-
import random
9-
from collections.abc import AsyncIterator
10-
from pathlib import Path
7+
import sys
8+
import time
119

10+
from create_image import iter_images
1211
from dotenv import load_dotenv
13-
from PIL import Image, ImageDraw
1412

1513
from athena_client.client.athena_client import AthenaClient
1614
from athena_client.client.athena_options import AthenaOptions
1715
from athena_client.client.channel import create_channel
18-
19-
EXAMPLE_IMAGES_DIR = Path(__file__).parent / "example_images"
20-
21-
22-
def create_random_image(width: int = 640, height: int = 480) -> bytes:
23-
"""Create a random test image with random shapes and colors.
24-
25-
Args:
26-
width: Width of the test image in pixels
27-
height: Height of the test image in pixels
28-
29-
Returns:
30-
JPEG image bytes
31-
32-
"""
33-
# Create a new image with random background color
34-
image = Image.new(
35-
"RGB",
36-
(width, height),
37-
(
38-
random.randint(0, 255),
39-
random.randint(0, 255),
40-
random.randint(0, 255),
41-
),
42-
)
43-
44-
draw = ImageDraw.Draw(image)
45-
46-
# Draw some random shapes
47-
for _ in range(3):
48-
# Random coordinates
49-
x0 = random.randint(0, width - 1)
50-
y0 = random.randint(0, height - 1)
51-
x1 = random.randint(0, width - 1)
52-
y1 = random.randint(0, height - 1)
53-
54-
# Random color
55-
color = (
56-
random.randint(0, 255),
57-
random.randint(0, 255),
58-
random.randint(0, 255),
59-
)
60-
61-
draw.rectangle(
62-
[min(x0, x1), min(y0, y1), max(x0, x1), max(y0, y1)], fill=color
63-
)
64-
65-
# Convert to JPEG bytes
66-
img_byte_arr = io.BytesIO()
67-
image.save(img_byte_arr, format="JPEG")
68-
return img_byte_arr.getvalue()
69-
70-
71-
async def iter_images(max_images: int | None = None) -> AsyncIterator[bytes]:
72-
"""Generate random test images.
73-
74-
Args:
75-
max_images: Maximum number of images to generate. If None, generates
76-
infinitely.
77-
78-
Yields:
79-
JPEG image bytes with random content
80-
81-
"""
82-
count = 0
83-
while max_images is None or count < max_images:
84-
yield create_random_image()
85-
count += 1
16+
from athena_client.client.deployment_selector import DeploymentSelector
8617

8718

8819
async def run_example(
@@ -104,37 +35,91 @@ async def run_example(
10435
channel = create_channel(options.host, auth_token)
10536

10637
async with AthenaClient(channel, options) as client:
38+
# Start image classification
10739
results = client.classify_images(iter_images(max_test_images))
108-
logger.info("Classifying images in image iter")
109-
async for result in results:
110-
result_msg = f"Get result {result=}"
111-
logger.info(result_msg)
112-
113-
114-
async def main() -> None:
40+
logger.info("Starting classification...")
41+
42+
# Initialize rate tracking
43+
start_time = time.time()
44+
response_count = 0
45+
46+
try:
47+
async for result in results:
48+
response_count += 1
49+
current_time = time.time()
50+
elapsed = current_time - start_time
51+
52+
if response_count % 10 == 0:
53+
rate = response_count / elapsed
54+
logger.info(
55+
"Processed %d responses (%.1f/sec)",
56+
response_count,
57+
rate,
58+
)
59+
60+
logger.debug("Got result: %s", result)
61+
except Exception:
62+
logger.exception("Error during classification")
63+
if response_count == 0:
64+
# Re-raise if we didn't process any responses
65+
raise
66+
finally:
67+
# Calculate final statistics after loop completes
68+
if response_count > 0:
69+
duration = time.time() - start_time
70+
avg_rate = response_count / duration
71+
logger.info(
72+
"\nFinal: %d responses in %.1f seconds (%.1f/sec)",
73+
response_count,
74+
duration,
75+
avg_rate,
76+
)
77+
78+
79+
async def main() -> int:
11580
"""Run examples showing both authenticated and unauthenticated usage."""
11681
logger = logging.getLogger(__name__)
11782
load_dotenv()
11883

11984
# Set maximum number of test images to generate (None for infinite)
120-
max_test_images = 5
85+
max_test_images = None
12186

12287
# Example with authenticated channel
12388
auth_token = os.getenv("ATHENA_AUTH_TOKEN")
124-
if auth_token:
125-
logger.info("Running example with secure authenticated channel...")
126-
auth_options = AthenaOptions(
127-
host=os.getenv("ATHENA_HOST", "localhost"),
128-
resize_images=True,
129-
deployment_id="argh",
130-
)
131-
await run_example(logger, auth_options, auth_token, max_test_images)
132-
else:
133-
logger.info(
134-
"Skipping authenticated example - ATHENA_AUTH_TOKEN not set"
135-
)
89+
if not auth_token:
90+
logger.error("ATHENA_AUTH_TOKEN not set.")
91+
return 1
92+
93+
logger.info("Running example with secure authenticated channel")
94+
host = os.getenv("ATHENA_HOST", "localhost")
95+
channel = create_channel(host, auth_token)
96+
97+
# Get the first available deployment using context manager
98+
async with DeploymentSelector(channel) as deployment_selector:
99+
deployments = await deployment_selector.list_deployments()
100+
101+
if not deployments.deployments:
102+
logger.error("No deployments available")
103+
return 1
104+
105+
deployment_id = deployments.deployments[0].deployment_id
106+
logger.info("Using deployment: %s", deployment_id)
107+
108+
athena_options = AthenaOptions(
109+
host=host,
110+
resize_images=True,
111+
deployment_id=deployment_id,
112+
compress_images=True,
113+
)
114+
await run_example(logger, athena_options, auth_token, max_test_images)
115+
116+
return 0
136117

137118

138119
if __name__ == "__main__":
139-
logging.basicConfig(level=logging.INFO)
140-
asyncio.run(main())
120+
logging.basicConfig(
121+
level=logging.DEBUG,
122+
format="%(asctime)s.%(msecs)03d %(levelname)s: %(message)s",
123+
datefmt="%H:%M:%S",
124+
)
125+
sys.exit(asyncio.run(main()))
-299 KB
Binary file not shown.
-585 KB
Binary file not shown.

pyproject.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
[project]
2-
3-
42
name = "athena_client"
53
version = "0.1.0"
64
description = "Athena Client Library"
@@ -14,7 +12,17 @@ dependencies = [
1412
"numpy>=2.2.6",
1513
"pillow>=11.3.0",
1614
]
15+
16+
[project.optional-dependencies]
1717
[dependency-groups]
18+
docs = [
19+
"sphinx>=7.1.0",
20+
"furo>=2024.1.29",
21+
"sphinx-autodoc-typehints>=1.25.0",
22+
"sphinx-copybutton>=0.5.0",
23+
"myst-parser>=2.0.0",
24+
]
25+
1826
dev = [
1927
"load-dotenv>=0.1.0",
2028
"mypy-protobuf>=3.6.0",

src/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Root package for athena-client.
2+
3+
This package contains the modules and packages that make up the athena-client
4+
library, which provides functionality for interacting with the Athena
5+
classification service.
6+
"""

0 commit comments

Comments
 (0)