Skip to content

Commit b4a7af7

Browse files
authored
Merge pull request #23 from TransitApp/add-pypi-publish-workflow
Add GitHub Action to publish to PyPI on version changes
2 parents bb8a325 + 39a9eb8 commit b4a7af7

File tree

9 files changed

+480
-29
lines changed

9 files changed

+480
-29
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* @npaun @JMilot1 @jsteelz
1+
* @JMilot1 @jsteelz

.github/workflows/publish.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: write
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 2
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v5
20+
21+
- name: Check if version changed
22+
id: version_check
23+
run: |
24+
VERSION=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
25+
echo "version=$VERSION" >> $GITHUB_OUTPUT
26+
27+
if git diff HEAD^ HEAD -- pyproject.toml | grep -q 'version ='; then
28+
echo "changed=true" >> $GITHUB_OUTPUT
29+
else
30+
echo "changed=false" >> $GITHUB_OUTPUT
31+
fi
32+
33+
- name: Build package
34+
if: steps.version_check.outputs.changed == 'true'
35+
run: uv build
36+
37+
- name: Publish to PyPI
38+
if: steps.version_check.outputs.changed == 'true'
39+
env:
40+
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
41+
run: uv publish
42+
43+
- name: Create GitHub Release
44+
if: steps.version_check.outputs.changed == 'true'
45+
env:
46+
GH_TOKEN: ${{ github.token }}
47+
run: |
48+
VERSION=${{ steps.version_check.outputs.version }}
49+
gh release create "v$VERSION" \
50+
--title "v$VERSION" \
51+
--generate-notes \
52+
dist/*

.github/workflows/pull-request.yml

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,19 @@ jobs:
1414
runs-on: [ubuntu-latest]
1515
strategy:
1616
matrix:
17-
python-version: [pypy-3.8]
17+
python-version: ['3.10', 'pypy3.10']
1818

1919
steps:
2020
- uses: actions/checkout@v2
21+
- name: Install uv
22+
uses: astral-sh/setup-uv@v5
2123
- name: Set up Python ${{ matrix.python-version }}
22-
uses: actions/setup-python@v2
23-
with:
24-
python-version: ${{ matrix.python-version }}
24+
run: uv python install ${{ matrix.python-version }}
2525
- name: Install dependencies
26-
run: |
27-
python -m pip install --upgrade pip
28-
python -m pip install flake8 pytest
29-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
26+
run: uv sync --all-extras --dev
3027
- name: Lint with flake8
3128
run: |
32-
# stop the build if there are Python syntax errors or undefined names
33-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
34-
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
35-
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
29+
uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv
30+
uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.venv
3631
- name: Test with pytest
37-
run: |
38-
python -m pytest .
32+
run: uv run pytest .

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10.19

CLAUDE.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
py-gtfs-loader is a Python library for loading and manipulating GTFS (General Transit Feed Specification) data. It parses GTFS directories into Python objects with schema validation and provides utilities for reading, modifying, and writing GTFS feeds.
8+
9+
## Development Commands
10+
11+
### Using uv (package manager)
12+
13+
```bash
14+
# Install dependencies
15+
uv sync --all-extras --dev
16+
17+
# Run tests
18+
uv run pytest .
19+
20+
# Run linting
21+
uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
22+
uv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
23+
24+
# Build package
25+
uv build
26+
27+
# Run a single test file
28+
uv run pytest tests/test_runner.py
29+
30+
# Run a specific test case
31+
uv run pytest tests/test_runner.py::test_default -k "test_name"
32+
```
33+
34+
## Architecture
35+
36+
### Core Components
37+
38+
**`gtfs_loader/__init__.py`** - Main entry point with load/patch functions
39+
- `load(gtfs_dir, ...)`: Parses GTFS directory into structured objects
40+
- `patch(gtfs, gtfs_in_dir, gtfs_out_dir, ...)`: Modifies and writes GTFS data back to disk
41+
- Supports both standard GTFS and Transit itinerary format via `itineraries=True` flag
42+
- CSV and GeoJSON file type support
43+
44+
**`gtfs_loader/schema.py`** - GTFS entity definitions and schemas
45+
- Defines all GTFS entities (Agency, Route, Trip, Stop, StopTime, etc.)
46+
- Entity classes have `_schema` attribute describing file structure (ID, grouping, required fields)
47+
- Two schema collections: `GTFS_SUBSET_SCHEMA` (standard) and `GTFS_SUBSET_SCHEMA_ITINERARIES` (Transit format)
48+
- Entities reference other entities via `_gtfs` attribute (e.g., `stop_time.stop` resolves to Stop object)
49+
50+
**`gtfs_loader/schema_classes.py`** - Schema metadata system
51+
- `File`: Describes GTFS file structure (primary key, grouping, file type)
52+
- `Field`: Named tuple for field configuration (type, required, default)
53+
- `FileCollection`: Container for file schemas
54+
- Grouping support: entities with same ID can be grouped by secondary key (e.g., stop_times grouped by trip_id + stop_sequence)
55+
56+
**`gtfs_loader/types.py`** - Custom types and base classes
57+
- `GTFSTime`: Integer-based time allowing >24h (e.g., "25:30:00" for next-day services)
58+
- `GTFSDate`: datetime subclass parsing YYYYMMDD and YYYY-MM-DD formats
59+
- `Entity`: Base class for all GTFS entities, dict-like with `_gtfs` reference to parent collection
60+
- `EntityDict`: Dict subclass storing resolved field metadata
61+
62+
### Data Flow
63+
64+
1. **Load**: CSV/GeoJSON → parse headers → validate fields → create Entity objects → index by ID → return nested dict structure
65+
2. **Access**: `gtfs.stops['stop_id']` or `gtfs.stop_times['trip_id'][sequence_index]`
66+
3. **Patch**: Flatten nested structures → write CSV with correct headers → preserve unmodified files
67+
68+
### Key Patterns
69+
70+
- **Entity indexing**: Primary entities indexed by `id` field, grouped entities create nested dicts/lists
71+
- **Cross-references**: Entities access related data via `_gtfs` backref (e.g., `trip.route`, `stop_time.stop`)
72+
- **Computed properties**: Use `@cached_property` for derived values (e.g., `trip.first_departure`)
73+
- **Two GTFS formats**: Standard (stop_times.txt) vs Transit itinerary format (itinerary_cells.txt + trip arrays)
74+
75+
## Itinerary Format Support
76+
77+
The library supports Transit's custom itinerary format where:
78+
- `itinerary_cells.txt` defines stop sequences (like templates)
79+
- Trips reference itineraries and contain time arrays instead of individual stop_times
80+
- Use `itineraries=True` flag when loading/patching to use this format

README.md

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,115 @@
11
# py-gtfs-loader
22

3-
Simple python library to load GTFS folder
3+
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
4+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5+
[![Build Status](https://github.com/TransitApp/py-gtfs-loader/workflows/Build%20on%20pull%20request/badge.svg)](https://github.com/TransitApp/py-gtfs-loader/actions)
46

5-
To use, simply `import gtfs_loader` and load a GTFS folder with `gtfs = gtfs_loader.load(args.gtfs_dir)`
7+
A Python library for loading and manipulating GTFS (General Transit Feed Specification) data with schema validation and type safety.
68

7-
All data is now available under `gtfs.filename`
9+
## Features
10+
11+
- 📦 Load GTFS feeds from directories
12+
- ✅ Schema validation with type checking
13+
- 🔄 Modify and patch GTFS data
14+
- 🚀 Support for standard GTFS and Transit's itinerary format
15+
- 📝 CSV and GeoJSON file type support
16+
- 🔗 Cross-referenced entities for easy data navigation
17+
18+
## Installation
19+
20+
```bash
21+
pip install py-gtfs-loader
22+
```
23+
24+
Or using uv:
25+
26+
```bash
27+
uv add py-gtfs-loader
28+
```
29+
30+
## Quick Start
31+
32+
### Loading GTFS Data
33+
34+
```python
35+
import gtfs_loader
36+
37+
# Load a GTFS feed
38+
gtfs = gtfs_loader.load('path/to/gtfs/directory')
39+
40+
# Access data by entity
41+
stop = gtfs.stops['stop_id']
42+
route = gtfs.routes['route_id']
43+
trip = gtfs.trips['trip_id']
44+
45+
# Access grouped entities
46+
stop_times = gtfs.stop_times['trip_id'] # Returns list of stop times for a trip
47+
```
48+
49+
### Modifying and Saving GTFS Data
50+
51+
```python
52+
# Modify data
53+
gtfs.stops['stop_id'].stop_name = "New Stop Name"
54+
55+
# Save changes back to disk
56+
gtfs_loader.patch(gtfs, 'path/to/input', 'path/to/output')
57+
```
58+
59+
### Loading Specific Files
60+
61+
```python
62+
# Load only specific files
63+
gtfs = gtfs_loader.load('path/to/gtfs', files=['stops', 'routes', 'trips'])
64+
```
65+
66+
### Transit Itinerary Format
67+
68+
```python
69+
# Load Transit itinerary format (itinerary_cells.txt)
70+
gtfs = gtfs_loader.load('path/to/gtfs', itineraries=True)
71+
```
72+
73+
## Development
74+
75+
This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
76+
77+
### Setup
78+
79+
```bash
80+
# Install dependencies
81+
uv sync --all-extras --dev
82+
83+
# Run tests
84+
uv run pytest .
85+
86+
# Run linting
87+
uv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
88+
```
89+
90+
### Requirements
91+
92+
- Python ≥ 3.10
93+
- Development dependencies: pytest, flake8
94+
95+
## Project Structure
96+
97+
- `gtfs_loader/` - Main package
98+
- `__init__.py` - Load/patch functions
99+
- `schema.py` - GTFS entity definitions
100+
- `schema_classes.py` - Schema metadata system
101+
- `types.py` - Custom GTFS types (GTFSTime, GTFSDate, Entity)
102+
- `lat_lon.py` - Geographic utilities
103+
104+
## Contributing
105+
106+
Contributions are welcome! Please feel free to submit a Pull Request.
107+
108+
## License
109+
110+
MIT License - see LICENSE file for details
111+
112+
## Maintainers
113+
114+
- Jonathan Milot
115+
- Jeremy Steele

pyproject.toml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
[project]
2+
name = "py-gtfs-loader"
3+
version = "0.3.0"
4+
description = "Load GTFS"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Jonathan Milot" },
8+
{ name = "Jeremy Steele" }
9+
]
10+
requires-python = ">=3.10"
11+
dependencies = []
12+
license = { text = "MIT" }
13+
classifiers = [
14+
"License :: OSI Approved :: MIT License",
15+
]
16+
17+
[project.urls]
18+
Homepage = "https://github.com/TransitApp/py-gtfs-loader"
19+
20+
[build-system]
21+
requires = ["setuptools>=61.0"]
22+
build-backend = "setuptools.build_meta"
23+
24+
[tool.setuptools.packages.find]
25+
include = ["gtfs_loader*"]
26+
27+
[dependency-groups]
28+
dev = [
29+
"flake8>=7.3.0",
30+
"pytest>=8.4.2",
31+
]

setup.py

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)