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
132 changes: 101 additions & 31 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,81 @@ on:
- cron: '31 13 * * 2'

jobs:
build:
# ── Offline unit tests ────────────────────────────────────────────────────
# Run on every supported Python version. No secrets required.
# Skips tests marked `live` or `integration` (see pytest.ini addopts).
test-offline:
name: "Unit tests – Python ${{ matrix.python-version }}"
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools pip pipenv
pipenv install --skip-lock --dev -e .
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
pipenv run ruff check . --select=E9,F63,F7,F82 --output-format=full
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
pipenv run ruff check . --exit-zero
- name: Test with pytest (offline only)
run: |
pipenv run py.test \
--cov-config .coveragerc \
--cov-report xml:output/coverage.xml \
--cov mygeotab \
--junitxml output/python${{ matrix.python-version }}-test-results.xml \
tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./output/coverage.xml
flags: "py${{ matrix.python-version }}"
- name: Archive test results
uses: actions/upload-artifact@v7
with:
name: "test-results-py${{ matrix.python-version }}"
path: output

# ── Live network tests ─────────────────────────────────────────────────────
# Unauthenticated calls to public Geotab servers (GetVersion).
# Runs on a single Python version; does not need DB credentials.
test-live:
name: "Live network tests – Python 3.12"
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v7
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools pip pipenv
pipenv install --skip-lock --dev -e .
- name: Run live server-call tests
run: |
pipenv run py.test -m live tests/

# ── Credentialed integration tests ────────────────────────────────────────
# Requires GitHub environment "test" with DB secrets. Runs sequentially
# (max-parallel: 1) to avoid hammering the shared test account.
test-integration:
name: "Integration tests – Python 3.12"
runs-on: ubuntu-22.04
environment: test
strategy:
max-parallel: 1
matrix:
python-version: [3.9]
env:
MYGEOTAB_DATABASE: ${{ secrets.MYGEOTAB_DATABASE }}
MYGEOTAB_USERNAME: ${{ secrets.MYGEOTAB_USERNAME }}
Expand All @@ -25,31 +93,33 @@ jobs:
MYGEOTAB_USERNAME_ASYNC: ${{ secrets.MYGEOTAB_USERNAME_ASYNC }}
MYGEOTAB_PASSWORD_ASYNC: ${{ secrets.MYGEOTAB_PASSWORD_ASYNC }}
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools pip pipenv
pipenv install --skip-lock --dev -e .
- name: Lint with ruff
run: |
# stop the build if there are Python syntax errors or undefined names
pipenv run ruff check . --select=E9,F63,F7,F82 --output-format=full
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
pipenv run ruff check . --exit-zero
- name: Test with pytest
run: |
pipenv run py.test --cov-config .coveragerc --cov-report xml:output/coverage.xml --cov mygeotab --junitxml output/python${{ matrix.python-version }}-test-results.xml --benchmark-min-rounds=3 --benchmark-storage=file://output/ --benchmark-autosave tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
with:
token: ${{secrets.CODECOV_TOKEN}}
file: ./output/coverage.xml
- name: Archive code coverage results
uses: actions/upload-artifact@v7
with:
name: output
path: output
- uses: actions/checkout@v7
- name: Set up Python 3.12
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade setuptools pip pipenv
pipenv install --skip-lock --dev -e .
- name: Run credentialed integration tests
# Skip if secrets are not available (e.g. PRs from forks).
# Exit code 5 means pytest collected 0 tests (no integration tests
# marked yet); treat that as success so the job doesn't block the PR.
run: |
if [ -z "$MYGEOTAB_DATABASE" ]; then
echo "No credentials available – skipping integration tests."
exit 0
fi
pipenv run py.test \
-m integration \
--benchmark-min-rounds=3 \
--benchmark-storage=file://output/ \
--benchmark-autosave \
--junitxml output/integration-test-results.xml \
tests/ || { code=$?; [ $code -eq 5 ] && echo "No integration tests collected – OK." || exit $code; }
- name: Archive integration results
uses: actions/upload-artifact@v7
with:
name: integration-results
path: output
98 changes: 98 additions & 0 deletions PR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Spring Cleaning PR

## Summary

Four independent improvements to package quality, CI reliability, and security — targeting customers who begin integrations through this package.

---

## 1. SDK documentation link

- **README.rst**: replace legacy `https://geotab.github.io/sdk/` with `https://developers.geotab.com/`
- **README.rst**: fix docs URL to HTTPS canonical form (`readthedocs.io/en/latest`)

---

## 2. Python version reconciliation (3.10–3.14)

Drops Python 3.9 (EOL October 2025) and adds 3.14. Every version contract now agrees:

| Location | Before | After |
|---|---|---|
| `setup.py` runtime guard | `< (3, 7, 0)` | `< (3, 10, 0)` |
| `setup.py` `python_requires` | missing | `>=3.10` |
| `setup.py` classifiers | 3.9–3.13 | 3.10–3.14 |
| `setup.py` packages list | `mygeotab`, `mygeotab.ext` | + `mygeotab.altitude` (was silently absent from all installs) |
| `mypy.ini` `python_version` | `9` (invalid) | `3.10` |
| `pyproject.toml` ruff `target-version` | `py39` | `py310` |
| `setup.cfg` | `[bdist_wheel] universal = 1` (Python 2 artifact) | removed |
| `README.rst` | "Python 3.9+" | "Python 3.10+" |

---

## 3. Test separation — offline vs credentialed, and full version matrix

**pytest.ini** — two new marks with descriptions; `addopts` excludes both by default so `pytest` never needs network access or credentials:
```ini
markers =
live: real network calls to public Geotab servers (no credentials)
integration: requires MYGEOTAB_DATABASE / USERNAME / PASSWORD
addopts = -m "not live and not integration"
```

**tests/test_api_live.py** — `pytestmark = pytest.mark.live` applied.

**.github/workflows/pythonpackage.yml** — single job replaced with three:

| Job | Python | Secrets | Runs |
|---|---|---|---|
| `test-offline` | 3.10 / 3.11 / 3.12 / 3.13 / 3.14 (matrix) | none | all unit tests |
| `test-live` | 3.12 | none | unauthenticated `GetVersion` calls |
| `test-integration` | 3.12 | DB credentials | credentialed tests; exits 0 when no tests collected (exit code 5) or secrets absent |

---

## 4. Session token file protection

`mygeotab/cli.py` — enforces strict filesystem permissions on every write:

- Config **directory**: `os.chmod(..., stat.S_IRWXU)` → **0700** (owner only)
- Config **file**: `os.chmod(..., stat.S_IRUSR | stat.S_IWUSR)` → **0600** (owner read/write only)
- Applied in both `Session.save()` and `Session.logout()`
- `myg --help` now includes guidance: *"To remove a saved session run: `myg sessions remove <database>`"*

---

## 5. Coverage: 59% → 81% (+22 pp)

835 lines of new tests across 5 files; 163 tests pass, 0 failures.

| File | Before | After |
|---|---|---|
| `altitude/daas_definition.py` | **0%** | **100%** |
| `exceptions.py` | ~90% | **100%** |
| `parameters.py` | ~95% | **100%** |
| `altitude/wrapper.py` | ~15% | **83%** |
| `api.py` | ~65% | **83%** |
| `ext/entitylist.py` | ~60% | **73%** |

**New file — `tests/test_daas_definition.py`**: full branch coverage of `DaasError`, `DaasResult` (None/empty/missing-`apiResult` guards, gateway errors, `apiResult` errors, `"error"` singular field, `errorMessage` as string/dict/empty), `DaasGetJobStatusResult` (all four `has_finished()` outcomes, `errorResult` override, missing `status` key), `DaasGetQueryResult`.

**Extended — `tests/test_altitude.py`**: `_extract_errors` (both branches), `call_api` (success, invalid name, non-retry exception), `create_job` (success, errors in response, re-raise), `check_job_status`, `wait_for_job_to_complete` (immediate done, polling, error path), `fetch_data` (single page, not-finished guard, multi-page), `get_data`, `do` (full orchestration).

**Extended — `tests/test_api_call.py`**: `authenticate()` `ExtendSession` path, `"ThisServer"` redirect, no-`path`-in-result, `DbUnavailableException` (`Initializing` + `UnknownDatabase`), other-exception re-raise; `call()` re-auth on `InvalidUserException` when password present, `__reauthorize_count` guard prevents infinite loop; `multi_call` with no params element.

**Extended — `tests/test_api.py`**: `_process()` unknown-structure passthrough; `_server` fallback to `my.geotab.com`; `MyGeotabException.data` field.

**Extended — `tests/test_api_entitylist.py`**: `first`/`last` on empty list, `entity` with 0 items, `__add__` plain list, `__radd__`, `__mul__`/`__rmul__`, `__copy__`, `to_dataframe` `ImportError`, `EntityListAPI.get` return type.

---

## Commits

| SHA | Message |
|---|---|
| `6e24073` | `chore: fix SDK link, Python version contracts, test separation, token file permissions` |
| `9854eab` | `fix(ci): treat pytest exit 5 (no integration tests) as success` |
| `3c955ce` | `test: raise offline coverage from 59% to 81%` |
| `c7ee84f` | `chore: expand supported Python range to 3.10–3.14` |
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ verify_ssl = true
coverage = "*"
pytest = "*"
pytest-cov = "*"
mygeotab = {editable = true,path = "."}
mypy = "*"
pytest-asyncio = "*"
requests-mock = "*"
Expand All @@ -17,6 +16,7 @@ types-requests = "*"
types-pytz = "*"
types-setuptools = "*"
ruff = "*"
mygeotab = {path = ".", editable = true}

[packages]
mygeotab = {editable = true, path = "."}
Expand Down
Loading
Loading