Skip to content

Commit d8dbf46

Browse files
authored
Merge pull request #4 from Geocodio/feature/ci-pipeline-tasks
Feature/ci pipeline tasks
2 parents 26c4d06 + 601d456 commit d8dbf46

File tree

5 files changed

+353
-64
lines changed

5 files changed

+353
-64
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,20 @@ jobs:
2727
uv pip install .
2828
uv pip install .[dev]
2929
30-
- name: Run tests
30+
- name: Run unit tests
3131
run: |
32-
uv run pytest
32+
uv run pytest tests/unit/
33+
34+
- name: Run e2e tests
35+
env:
36+
GEOCODIO_API_KEY: ${{ secrets.GEOCODIO_API_KEY }}
37+
run: |
38+
if [ -n "$GEOCODIO_API_KEY" ]; then
39+
echo "Running e2e tests with API key"
40+
uv run pytest tests/e2e/ -v
41+
else
42+
echo "No API key provided, skipping e2e tests"
43+
fi
3344
3445
- name: Lint with flake8
3546
run: |

.github/workflows/publish.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: 'Version to publish (e.g., 1.0.0)'
10+
required: true
11+
default: '1.0.0'
12+
publish_to:
13+
description: 'Where to publish'
14+
required: true
15+
default: 'testpypi'
16+
type: choice
17+
options:
18+
- testpypi
19+
- pypi
20+
21+
jobs:
22+
publish:
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Install uv
29+
uses: astral-sh/setup-uv@v5
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v5
33+
with:
34+
python-version: "3.11"
35+
36+
- name: Install dependencies
37+
run: |
38+
uv sync
39+
uv pip install build twine
40+
41+
- name: Update version
42+
if: github.event.inputs.version
43+
run: |
44+
# Update version in pyproject.toml
45+
sed -i 's/^version = ".*"/version = "${{ github.event.inputs.version }}"/' pyproject.toml
46+
47+
- name: Build package
48+
run: |
49+
uv run python -m build
50+
51+
- name: Publish to TestPyPI
52+
if: github.event.inputs.publish_to == 'testpypi' || github.event_name == 'release'
53+
env:
54+
TWINE_USERNAME: __token__
55+
TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
56+
TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/
57+
run: |
58+
uv run twine upload --verbose dist/*
59+
60+
- name: Publish to PyPI
61+
if: github.event.inputs.publish_to == 'pypi'
62+
env:
63+
TWINE_USERNAME: __token__
64+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
65+
run: |
66+
uv run twine upload --verbose dist/*

README.md

Lines changed: 71 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,13 @@ Development Installation
2929

3030
2. Create and activate a virtual environment:
3131
```bash
32-
python -m .venv venv
33-
source .venv/bin/activate # On Windows: venv\Scripts\activate
32+
python -m venv venv
33+
source venv/bin/activate # On Windows: venv\Scripts\activate
3434
```
3535

3636
3. Install development dependencies:
3737
```bash
38-
pip install .
39-
pip install .[dev]
38+
pip install -e .
4039
pip install -r requirements-dev.txt
4140
```
4241

@@ -52,27 +51,28 @@ from geocodio import GeocodioClient
5251
client = GeocodioClient("YOUR_API_KEY")
5352
5453
# Single forward geocode
55-
response = client.geocode("123 Anywhere St, Chicago, IL")
56-
print(response)
54+
response = client.geocode("1600 Pennsylvania Ave, Washington, DC")
55+
print(response.results[0].formatted_address)
5756
5857
# Batch forward geocode
5958
addresses = [
60-
"123 Anywhere St, Chicago, IL",
61-
"456 Oak St, Los Angeles, CA"
59+
"1600 Pennsylvania Ave, Washington, DC",
60+
"1 Infinite Loop, Cupertino, CA"
6261
]
6362
batch_response = client.geocode(addresses)
64-
print(batch_response)
63+
for result in batch_response.results:
64+
print(result.formatted_address)
6565
6666
# Single reverse geocode
6767
rev = client.reverse("38.9002898,-76.9990361")
68-
print(rev)
68+
print(rev.results[0].formatted_address)
6969
7070
# Append additional fields
7171
data = client.geocode(
72-
"123 Anywhere St, Chicago, IL",
72+
"1600 Pennsylvania Ave, Washington, DC",
7373
fields=["cd", "timezone"]
7474
)
75-
print(data)
75+
print(data.results[0].fields.timezone.name if data.results[0].fields.timezone else "No timezone data")
7676
```
7777
7878
### List API
@@ -85,68 +85,50 @@ from geocodio import GeocodioClient
8585
# Initialize the client with your API key
8686
client = GeocodioClient("YOUR_API_KEY")
8787
88-
# Create a new list
89-
new_list = client.create_list(
90-
name="My Addresses",
91-
description="A list of addresses to geocode",
92-
items=[
93-
"1600 Pennsylvania Ave, Washington, DC",
94-
"1 Infinite Loop, Cupertino, CA"
95-
]
96-
)
97-
print(new_list)
98-
9988
# Get all lists
10089
lists = client.get_lists()
101-
print(lists)
90+
print(f"Found {len(lists.data)} lists")
10291
103-
# Get a specific list
104-
list_id = new_list.list.id
105-
list_details = client.get_list(list_id)
106-
print(list_details)
107-
108-
# Update a list
109-
updated_list = client.update_list(
110-
list_id=list_id,
111-
name="Updated List Name",
112-
description="Updated description"
113-
)
114-
print(updated_list)
115-
116-
# Add items to a list
117-
added_items = client.add_items_to_list(
118-
list_id=list_id,
119-
items=[
120-
"123 Anywhere St, Chicago, IL",
121-
"456 Oak St, Los Angeles, CA"
122-
]
123-
)
124-
print(added_items)
92+
# Create a new list from a file
93+
with open("addresses.csv", "rb") as f:
94+
new_list = client.create_list(
95+
file=f,
96+
filename="addresses.csv",
97+
direction="forward"
98+
)
99+
print(f"Created list: {new_list.id}")
125100
126-
# Geocode all items in a list
127-
geocoded_list = client.geocode_list(
128-
list_id=list_id,
129-
fields=["timezone", "cd"]
130-
)
131-
print(geocoded_list)
101+
# Get a specific list
102+
list_details = client.get_list(new_list.id)
103+
print(f"List status: {list_details.status}")
132104
133-
# Remove items from a list
134-
item_ids = [item.id for item in added_items.items]
135-
client.remove_items_from_list(
136-
list_id=list_id,
137-
item_ids=item_ids
138-
)
105+
# Download a completed list
106+
if list_details.status and list_details.status.get("state") == "COMPLETED":
107+
file_content = client.download(new_list.id, "downloaded_results.csv")
108+
print("List downloaded successfully")
139109
140110
# Delete a list
141-
client.delete_list(list_id)
111+
client.delete_list(new_list.id)
142112
```
143113
144114
Error Handling
145115
--------------
146116
147-
- `GeocodioAuthError` is raised for authentication failures (HTTP 403).
148-
- `GeocodioDataError` is raised for invalid requests (HTTP 422).
149-
- `GeocodioServerError` is raised for server-side errors (HTTP 5xx).
117+
```python
118+
from geocodio import GeocodioClient, AuthenticationError, InvalidRequestError
119+
120+
try:
121+
client = GeocodioClient("INVALID_API_KEY")
122+
response = client.geocode("1600 Pennsylvania Ave, Washington, DC")
123+
except AuthenticationError as e:
124+
print(f"Authentication failed: {e}")
125+
126+
try:
127+
client = GeocodioClient("YOUR_API_KEY")
128+
response = client.geocode("") # Empty address
129+
except InvalidRequestError as e:
130+
print(f"Invalid request: {e}")
131+
```
150132
151133
Documentation
152134
-------------
@@ -164,3 +146,30 @@ License
164146
-------
165147
166148
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
149+
150+
CI & Publishing
151+
---------------
152+
153+
- CI runs unit tests and linting on every push. E2E tests run if `GEOCODIO_API_KEY` is set as a secret.
154+
- PyPI publishing workflow supports both TestPyPI and PyPI. See `.github/workflows/publish.yml`.
155+
- Use `test_pypi_release.py` for local packaging and dry-run upload.
156+
157+
### Testing GitHub Actions Workflows
158+
159+
The project includes tests for GitHub Actions workflows using `act` for local development:
160+
161+
```bash
162+
# Test all workflows (requires act and Docker)
163+
pytest tests/test_workflows.py
164+
165+
# Test specific workflow
166+
pytest tests/test_workflows.py::test_ci_workflow
167+
pytest tests/test_workflows.py::test_publish_workflow
168+
```
169+
170+
**Prerequisites:**
171+
- Install [act](https://github.com/nektos/act) for local GitHub Actions testing
172+
- Docker must be running
173+
- For publish workflow tests: Set `TEST_PYPI_API_TOKEN` environment variable
174+
175+
**Note:** Workflow tests are automatically skipped in CI environments.

test_pypi_release.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test script for PyPI release process
4+
"""
5+
6+
import os
7+
import subprocess
8+
import sys
9+
10+
def run_command(cmd, description):
11+
"""Run a command and handle errors"""
12+
print(f"\n{description}...")
13+
print(f"Running: {cmd}")
14+
15+
try:
16+
result = subprocess.run(cmd, shell=True, check=True, capture_output=True, text=True)
17+
print(f"✓ {description} successful")
18+
if result.stdout:
19+
print(f"Output: {result.stdout}")
20+
return True
21+
except subprocess.CalledProcessError as e:
22+
print(f"✗ {description} failed")
23+
print(f"Error: {e.stderr}")
24+
return False
25+
26+
def main():
27+
print("Testing PyPI release process...")
28+
29+
# Step 1: Build the package
30+
if not run_command("python -m build", "Building package"):
31+
return False
32+
33+
# Step 2: Check the built files
34+
if not os.path.isdir("dist/"):
35+
print("✗ Build directory not found")
36+
return False
37+
38+
files = os.listdir("dist/")
39+
print(f"✓ Built files: {files}")
40+
41+
# Step 3: Check package with twine
42+
if not run_command("twine check dist/*", "Checking package with twine"):
43+
return False
44+
45+
# Step 4: Test upload to TestPyPI (dry run)
46+
print("\nTesting upload to TestPyPI (dry run)...")
47+
print("Note: This requires TEST_PYPI_API_TOKEN to be set")
48+
49+
test_pypi_token = os.getenv("TEST_PYPI_API_TOKEN")
50+
if not test_pypi_token:
51+
print("⚠ TEST_PYPI_API_TOKEN not set - skipping upload test")
52+
print("To test upload, set TEST_PYPI_API_TOKEN environment variable")
53+
return True
54+
55+
# Test upload to TestPyPI
56+
upload_cmd = "twine upload --repository testpypi --verbose dist/*"
57+
if not run_command(upload_cmd, "Uploading to TestPyPI"):
58+
return False
59+
60+
print("\n✓ PyPI release process test completed successfully!")
61+
return True
62+
63+
if __name__ == "__main__":
64+
success = main()
65+
sys.exit(0 if success else 1)

0 commit comments

Comments
 (0)