queenscoach 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- queenscoach-1.1.0/.dockerignore +15 -0
- queenscoach-1.1.0/.env.example +23 -0
- queenscoach-1.1.0/.github/workflows/ci.yml +104 -0
- queenscoach-1.1.0/.github/workflows/release.yml +119 -0
- queenscoach-1.1.0/.gitignore +14 -0
- queenscoach-1.1.0/Dockerfile +27 -0
- queenscoach-1.1.0/LICENSE +21 -0
- queenscoach-1.1.0/PKG-INFO +416 -0
- queenscoach-1.1.0/README.md +392 -0
- queenscoach-1.1.0/deploy/GCP.md +317 -0
- queenscoach-1.1.0/deploy/README.md +233 -0
- queenscoach-1.1.0/deploy/gcp-spend-cap/main.py +106 -0
- queenscoach-1.1.0/deploy/gcp-spend-cap/requirements.txt +3 -0
- queenscoach-1.1.0/deploy/queenscoach.env.example +22 -0
- queenscoach-1.1.0/deploy/queenscoach.service +45 -0
- queenscoach-1.1.0/pyproject.toml +66 -0
- queenscoach-1.1.0/scripts/deploy-gcp.sh +819 -0
- queenscoach-1.1.0/scripts/install.sh +685 -0
- queenscoach-1.1.0/server.json +29 -0
- queenscoach-1.1.0/src/queenscoach/__init__.py +6 -0
- queenscoach-1.1.0/src/queenscoach/__main__.py +6 -0
- queenscoach-1.1.0/src/queenscoach/cache.py +84 -0
- queenscoach-1.1.0/src/queenscoach/config.py +444 -0
- queenscoach-1.1.0/src/queenscoach/feed_http.py +66 -0
- queenscoach-1.1.0/src/queenscoach/gtfs_csv.py +41 -0
- queenscoach-1.1.0/src/queenscoach/http.py +193 -0
- queenscoach-1.1.0/src/queenscoach/main.py +145 -0
- queenscoach-1.1.0/src/queenscoach/oauth.py +410 -0
- queenscoach-1.1.0/src/queenscoach/realtime.py +298 -0
- queenscoach-1.1.0/src/queenscoach/server.py +178 -0
- queenscoach-1.1.0/src/queenscoach/static_gtfs.py +171 -0
- queenscoach-1.1.0/src/queenscoach/token_store.py +224 -0
- queenscoach-1.1.0/src/queenscoach/token_store_firestore.py +310 -0
- queenscoach-1.1.0/src/queenscoach/tools.py +363 -0
- queenscoach-1.1.0/src/queenscoach/transit.py +354 -0
- queenscoach-1.1.0/tests/__init__.py +1 -0
- queenscoach-1.1.0/tests/conftest.py +92 -0
- queenscoach-1.1.0/tests/fixtures/Alerts.pb +0 -0
- queenscoach-1.1.0/tests/fixtures/TripUpdates.pb +0 -0
- queenscoach-1.1.0/tests/fixtures/VehiclePositions.pb +0 -0
- queenscoach-1.1.0/tests/fixtures/gtfs-static.zip +0 -0
- queenscoach-1.1.0/tests/test_cache.py +110 -0
- queenscoach-1.1.0/tests/test_config.py +256 -0
- queenscoach-1.1.0/tests/test_feed_http.py +125 -0
- queenscoach-1.1.0/tests/test_gtfs_csv.py +36 -0
- queenscoach-1.1.0/tests/test_http.py +323 -0
- queenscoach-1.1.0/tests/test_main.py +57 -0
- queenscoach-1.1.0/tests/test_oauth.py +349 -0
- queenscoach-1.1.0/tests/test_realtime.py +69 -0
- queenscoach-1.1.0/tests/test_server.py +89 -0
- queenscoach-1.1.0/tests/test_static_gtfs.py +64 -0
- queenscoach-1.1.0/tests/test_stdio.py +71 -0
- queenscoach-1.1.0/tests/test_token_store.py +272 -0
- queenscoach-1.1.0/tests/test_tools.py +194 -0
- queenscoach-1.1.0/tests/test_transit.py +96 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# HTTP transport with Google OAuth. Copy to .env and fill in; .env is gitignored.
|
|
2
|
+
# Not needed for the stdio transport.
|
|
3
|
+
|
|
4
|
+
QUEENSCOACH_TRANSPORT=http
|
|
5
|
+
|
|
6
|
+
# From an OAuth 2.0 "Web application" client at
|
|
7
|
+
# https://console.cloud.google.com/apis/credentials
|
|
8
|
+
# Its authorized redirect URI must be exactly <QUEENSCOACH_PUBLIC_URL>/auth/google/callback
|
|
9
|
+
QUEENSCOACH_GOOGLE_CLIENT_ID=
|
|
10
|
+
QUEENSCOACH_GOOGLE_CLIENT_SECRET=
|
|
11
|
+
|
|
12
|
+
# Who may use the server. At least one of these three is required; without one
|
|
13
|
+
# the server refuses to start rather than admitting every Google account.
|
|
14
|
+
QUEENSCOACH_ALLOWED_EMAILS=you@example.com
|
|
15
|
+
# QUEENSCOACH_ALLOWED_DOMAINS=example.com
|
|
16
|
+
# QUEENSCOACH_ALLOW_ANY_GOOGLE_ACCOUNT=false
|
|
17
|
+
|
|
18
|
+
# The externally reachable origin: what clients dial, and this server's OAuth
|
|
19
|
+
# issuer. Must be https unless it is loopback.
|
|
20
|
+
QUEENSCOACH_PUBLIC_URL=https://queenscoach.example.com
|
|
21
|
+
|
|
22
|
+
# QUEENSCOACH_HTTP_HOST=127.0.0.1
|
|
23
|
+
# QUEENSCOACH_HTTP_PORT=8000
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
# So the release workflow can gate a publish on this same suite.
|
|
9
|
+
workflow_call:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
# A newer push to the same branch makes an in-flight run redundant.
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ci-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
fail-fast: false
|
|
24
|
+
matrix:
|
|
25
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v5
|
|
28
|
+
- uses: actions/setup-python@v6
|
|
29
|
+
with:
|
|
30
|
+
python-version: ${{ matrix.python-version }}
|
|
31
|
+
cache: pip
|
|
32
|
+
cache-dependency-path: pyproject.toml
|
|
33
|
+
- run: pip install -e ".[dev]"
|
|
34
|
+
- run: pytest
|
|
35
|
+
|
|
36
|
+
lint:
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v5
|
|
40
|
+
- uses: actions/setup-python@v6
|
|
41
|
+
with:
|
|
42
|
+
# The version pyproject targets for ruff and mypy.
|
|
43
|
+
python-version: "3.11"
|
|
44
|
+
cache: pip
|
|
45
|
+
cache-dependency-path: pyproject.toml
|
|
46
|
+
- run: pip install -e ".[dev]"
|
|
47
|
+
- run: ruff check
|
|
48
|
+
- run: ruff format --check
|
|
49
|
+
- run: mypy
|
|
50
|
+
|
|
51
|
+
# stdio exactly as a PyPI or MCP-registry user gets it: no optional extras.
|
|
52
|
+
stdio-plain-install:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v5
|
|
56
|
+
- uses: actions/setup-python@v6
|
|
57
|
+
with:
|
|
58
|
+
python-version: "3.11"
|
|
59
|
+
- run: pip install . pytest pytest-asyncio
|
|
60
|
+
- name: The gcp extra must not arrive with a plain install
|
|
61
|
+
run: |
|
|
62
|
+
python - <<'PY'
|
|
63
|
+
import importlib.util, sys
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
found = importlib.util.find_spec("google.cloud.firestore") is not None
|
|
67
|
+
except ModuleNotFoundError:
|
|
68
|
+
found = False
|
|
69
|
+
sys.exit("google-cloud-firestore is installed without the gcp extra" if found else 0)
|
|
70
|
+
PY
|
|
71
|
+
- run: queenscoach --version
|
|
72
|
+
- run: pytest tests/test_stdio.py
|
|
73
|
+
|
|
74
|
+
# The Firestore token store against the emulator; the main suite skips it.
|
|
75
|
+
firestore:
|
|
76
|
+
runs-on: ubuntu-latest
|
|
77
|
+
env:
|
|
78
|
+
FIRESTORE_EMULATOR_HOST: 127.0.0.1:8085
|
|
79
|
+
steps:
|
|
80
|
+
- uses: actions/checkout@v5
|
|
81
|
+
- uses: actions/setup-python@v6
|
|
82
|
+
with:
|
|
83
|
+
python-version: "3.11"
|
|
84
|
+
cache: pip
|
|
85
|
+
cache-dependency-path: pyproject.toml
|
|
86
|
+
# The emulator is a Java program shipped as a gcloud component.
|
|
87
|
+
- uses: actions/setup-java@v4
|
|
88
|
+
with:
|
|
89
|
+
distribution: temurin
|
|
90
|
+
java-version: "21"
|
|
91
|
+
- uses: google-github-actions/setup-gcloud@v2
|
|
92
|
+
with:
|
|
93
|
+
install_components: beta,cloud-firestore-emulator
|
|
94
|
+
- run: pip install -e ".[dev]"
|
|
95
|
+
- name: Start the Firestore emulator
|
|
96
|
+
run: |
|
|
97
|
+
gcloud emulators firestore start --host-port="$FIRESTORE_EMULATOR_HOST" > emulator.log 2>&1 &
|
|
98
|
+
for _ in $(seq 1 60); do
|
|
99
|
+
curl -sf "http://$FIRESTORE_EMULATOR_HOST" > /dev/null && exit 0
|
|
100
|
+
sleep 1
|
|
101
|
+
done
|
|
102
|
+
cat emulator.log
|
|
103
|
+
exit 1
|
|
104
|
+
- run: pytest tests/test_token_store.py
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Cutting a version means pushing a tag: `git tag v1.0.1 && git push origin v1.0.1`.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
# PyPI refuses a re-upload, so a version mismatch is unrecoverable under that
|
|
13
|
+
# tag. Catch it before anything is built.
|
|
14
|
+
check-version:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
|
+
- uses: actions/setup-python@v6
|
|
19
|
+
with:
|
|
20
|
+
# tomllib needs 3.11+; don't depend on the runner's default.
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
- name: Tag must match pyproject.toml, server.json, and SERVER_VERSION
|
|
23
|
+
run: |
|
|
24
|
+
python - <<'PY'
|
|
25
|
+
import json, os, re, sys, tomllib
|
|
26
|
+
|
|
27
|
+
tag = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
28
|
+
server = json.load(open("server.json"))
|
|
29
|
+
# What the server reports to clients in its MCP initialize response.
|
|
30
|
+
init = open("src/queenscoach/__init__.py").read()
|
|
31
|
+
reported = re.search(r'^SERVER_VERSION = "([^"]+)"', init, re.MULTILINE)
|
|
32
|
+
found = {
|
|
33
|
+
"pyproject.toml": tomllib.load(open("pyproject.toml", "rb"))["project"]["version"],
|
|
34
|
+
"server.json (version)": server["version"],
|
|
35
|
+
"server.json (packages[0].version)": server["packages"][0]["version"],
|
|
36
|
+
"src/queenscoach/__init__.py (SERVER_VERSION)": reported.group(1) if reported else None,
|
|
37
|
+
}
|
|
38
|
+
bad = {where: got for where, got in found.items() if got != tag}
|
|
39
|
+
for where, got in bad.items():
|
|
40
|
+
print(f"::error::tag is {tag} but {where} is {got}")
|
|
41
|
+
sys.exit(1 if bad else 0)
|
|
42
|
+
PY
|
|
43
|
+
|
|
44
|
+
test:
|
|
45
|
+
needs: check-version
|
|
46
|
+
uses: ./.github/workflows/ci.yml
|
|
47
|
+
|
|
48
|
+
publish:
|
|
49
|
+
needs: test
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
# Must match the environment named in the PyPI trusted publisher.
|
|
52
|
+
environment: pypi
|
|
53
|
+
permissions:
|
|
54
|
+
# Mints the short-lived OIDC token PyPI trusts. No API token to store.
|
|
55
|
+
id-token: write
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v5
|
|
58
|
+
- uses: actions/setup-python@v6
|
|
59
|
+
with:
|
|
60
|
+
python-version: "3.11"
|
|
61
|
+
- run: pip install build
|
|
62
|
+
- run: python -m build
|
|
63
|
+
- name: Check the built distributions
|
|
64
|
+
run: pipx run twine check --strict dist/*
|
|
65
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
66
|
+
- uses: actions/upload-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: dist
|
|
69
|
+
path: dist/
|
|
70
|
+
|
|
71
|
+
publish-registry:
|
|
72
|
+
needs: publish
|
|
73
|
+
runs-on: ubuntu-latest
|
|
74
|
+
permissions:
|
|
75
|
+
# mcp-publisher trades this for a registry token; no secret to store.
|
|
76
|
+
id-token: write
|
|
77
|
+
contents: read
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v5
|
|
80
|
+
- name: Wait for the release to be visible on PyPI
|
|
81
|
+
# The registry verifies the mcp-name marker by reading the package
|
|
82
|
+
# description off PyPI, so it has to be able to see this version.
|
|
83
|
+
run: |
|
|
84
|
+
version="${GITHUB_REF_NAME#v}"
|
|
85
|
+
for attempt in $(seq 1 20); do
|
|
86
|
+
if curl -sf "https://pypi.org/pypi/queenscoach/$version/json" > /dev/null; then
|
|
87
|
+
echo "queenscoach $version is on PyPI"
|
|
88
|
+
exit 0
|
|
89
|
+
fi
|
|
90
|
+
echo "attempt $attempt: not visible yet, retrying in 15s"
|
|
91
|
+
sleep 15
|
|
92
|
+
done
|
|
93
|
+
echo "::error::queenscoach $version never appeared on PyPI"
|
|
94
|
+
exit 1
|
|
95
|
+
- name: Install mcp-publisher
|
|
96
|
+
run: |
|
|
97
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
98
|
+
- name: Authenticate to the MCP Registry
|
|
99
|
+
run: ./mcp-publisher login github-oidc
|
|
100
|
+
- name: Publish to the MCP Registry
|
|
101
|
+
run: ./mcp-publisher publish
|
|
102
|
+
|
|
103
|
+
github-release:
|
|
104
|
+
needs: [publish, publish-registry]
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
permissions:
|
|
107
|
+
# Creating the release writes to the repo.
|
|
108
|
+
contents: write
|
|
109
|
+
steps:
|
|
110
|
+
# Only so gh infers the repository; the notes come from the API.
|
|
111
|
+
- uses: actions/checkout@v5
|
|
112
|
+
- uses: actions/download-artifact@v4
|
|
113
|
+
with:
|
|
114
|
+
name: dist
|
|
115
|
+
path: dist/
|
|
116
|
+
- name: Create the GitHub release
|
|
117
|
+
env:
|
|
118
|
+
GH_TOKEN: ${{ github.token }}
|
|
119
|
+
run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# The HTTP transport for a hosted deployment, as scripts/deploy-gcp.sh builds it
|
|
2
|
+
# on Cloud Build. The stdio server does not need this: `pip install queenscoach`.
|
|
3
|
+
FROM python:3.12-slim
|
|
4
|
+
|
|
5
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
6
|
+
PYTHONUNBUFFERED=1 \
|
|
7
|
+
PIP_NO_CACHE_DIR=1 \
|
|
8
|
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
9
|
+
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
12
|
+
COPY src ./src
|
|
13
|
+
# The gcp extra adds the Firestore token store (QUEENSCOACH_TOKEN_STORE=firestore).
|
|
14
|
+
RUN pip install '.[gcp]' \
|
|
15
|
+
&& useradd --system --no-create-home --uid 10001 queenscoach
|
|
16
|
+
|
|
17
|
+
USER queenscoach
|
|
18
|
+
|
|
19
|
+
# Cloud Run delivers traffic to port 8080 by default. The server binds every
|
|
20
|
+
# interface because the platform's front end is the only way in; it still
|
|
21
|
+
# requires QUEENSCOACH_PUBLIC_URL, the Google client, and an allow list at runtime.
|
|
22
|
+
ENV QUEENSCOACH_TRANSPORT=http \
|
|
23
|
+
QUEENSCOACH_HTTP_HOST=0.0.0.0 \
|
|
24
|
+
QUEENSCOACH_HTTP_PORT=8080
|
|
25
|
+
|
|
26
|
+
EXPOSE 8080
|
|
27
|
+
CMD ["queenscoach"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Wanninger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|