osm-sdk 0.1.0.dev1__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.
- osm_sdk-0.1.0.dev1/.github/workflows/ci.yml +45 -0
- osm_sdk-0.1.0.dev1/.github/workflows/lint-pr-title.yml +37 -0
- osm_sdk-0.1.0.dev1/.github/workflows/publish.yml +37 -0
- osm_sdk-0.1.0.dev1/.gitignore +29 -0
- osm_sdk-0.1.0.dev1/CONTRIBUTING.md +49 -0
- osm_sdk-0.1.0.dev1/LICENSE +21 -0
- osm_sdk-0.1.0.dev1/PKG-INFO +75 -0
- osm_sdk-0.1.0.dev1/PUBLIC_API.md +114 -0
- osm_sdk-0.1.0.dev1/README.md +57 -0
- osm_sdk-0.1.0.dev1/implementation-plan.md +31 -0
- osm_sdk-0.1.0.dev1/osm_sdk/__init__.py +40 -0
- osm_sdk-0.1.0.dev1/osm_sdk/auth.py +74 -0
- osm_sdk-0.1.0.dev1/osm_sdk/client.py +263 -0
- osm_sdk-0.1.0.dev1/osm_sdk/endpoints.py +80 -0
- osm_sdk-0.1.0.dev1/osm_sdk/errors.py +22 -0
- osm_sdk-0.1.0.dev1/osm_sdk/http.py +26 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/__init__.py +15 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/common.py +159 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/contacts.py +190 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/events.py +133 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/member_type.py +28 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/members.py +56 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/programme.py +126 -0
- osm_sdk-0.1.0.dev1/osm_sdk/mappers/startup.py +82 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/__init__.py +24 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/attendance.py +10 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/attendance_status.py +9 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/base.py +24 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/contact.py +17 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/contact_type.py +9 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/event.py +26 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/meeting.py +31 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/member.py +33 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/member_type.py +9 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/section.py +46 -0
- osm_sdk-0.1.0.dev1/osm_sdk/models/term.py +49 -0
- osm_sdk-0.1.0.dev1/osm_sdk.egg-info/PKG-INFO +75 -0
- osm_sdk-0.1.0.dev1/osm_sdk.egg-info/SOURCES.txt +53 -0
- osm_sdk-0.1.0.dev1/osm_sdk.egg-info/dependency_links.txt +1 -0
- osm_sdk-0.1.0.dev1/osm_sdk.egg-info/requires.txt +10 -0
- osm_sdk-0.1.0.dev1/osm_sdk.egg-info/top_level.txt +1 -0
- osm_sdk-0.1.0.dev1/pyproject.toml +46 -0
- osm_sdk-0.1.0.dev1/setup.cfg +4 -0
- osm_sdk-0.1.0.dev1/tests/__init__.py +1 -0
- osm_sdk-0.1.0.dev1/tests/conftest.py +18 -0
- osm_sdk-0.1.0.dev1/tests/fixtures/oauth_resource_sample.json +30 -0
- osm_sdk-0.1.0.dev1/tests/integration/__init__.py +1 -0
- osm_sdk-0.1.0.dev1/tests/integration/test_live_auth_smoke.py +21 -0
- osm_sdk-0.1.0.dev1/tests/integration/test_live_section_lookup.py +22 -0
- osm_sdk-0.1.0.dev1/tests/unit/__init__.py +1 -0
- osm_sdk-0.1.0.dev1/tests/unit/support.py +285 -0
- osm_sdk-0.1.0.dev1/tests/unit/test_client_discovery.py +278 -0
- osm_sdk-0.1.0.dev1/tests/unit/test_events_and_attendance.py +121 -0
- osm_sdk-0.1.0.dev1/tests/unit/test_members_and_contacts.py +133 -0
- osm_sdk-0.1.0.dev1/tests/unit/test_programme_and_meetings.py +106 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Build and Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build-and-test:
|
|
11
|
+
name: Build and Test
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout
|
|
15
|
+
uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
- name: Setup Python
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.11"
|
|
23
|
+
cache: "pip"
|
|
24
|
+
cache-dependency-path: pyproject.toml
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade pip
|
|
29
|
+
python -m pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Lint
|
|
32
|
+
run: ruff check .
|
|
33
|
+
|
|
34
|
+
- name: Test
|
|
35
|
+
run: pytest -q
|
|
36
|
+
|
|
37
|
+
- name: Build package
|
|
38
|
+
run: python -m build
|
|
39
|
+
|
|
40
|
+
- name: Upload distribution artifacts
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: python-dist
|
|
44
|
+
path: dist/*
|
|
45
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Lint Pull Request Title
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- reopened
|
|
8
|
+
- edited
|
|
9
|
+
- synchronize
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
pull-request-title-lint:
|
|
16
|
+
name: Lint Pull Request Title
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- name: Lint Pull Request Title
|
|
20
|
+
id: lint_pr_title
|
|
21
|
+
uses: amannn/action-semantic-pull-request@v6
|
|
22
|
+
env:
|
|
23
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
24
|
+
|
|
25
|
+
- name: Sticky Pull Request Comment
|
|
26
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
27
|
+
if: always() && (steps.lint_pr_title.outputs.error_message != null)
|
|
28
|
+
with:
|
|
29
|
+
header: pr-title-lint-error
|
|
30
|
+
message: Pull request titles must follow the Conventional Commits specification.
|
|
31
|
+
|
|
32
|
+
- name: Delete Sticky Pull Request Comment
|
|
33
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
34
|
+
if: ${{ steps.lint_pr_title.outputs.error_message == null }}
|
|
35
|
+
with:
|
|
36
|
+
header: pr-title-lint-error
|
|
37
|
+
delete: true
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [ published ]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
name: Publish to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write
|
|
14
|
+
contents: read
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout
|
|
17
|
+
uses: actions/checkout@v6
|
|
18
|
+
with:
|
|
19
|
+
fetch-depth: 0
|
|
20
|
+
|
|
21
|
+
- name: Setup Python
|
|
22
|
+
uses: actions/setup-python@v6
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
cache: "pip"
|
|
26
|
+
cache-dependency-path: pyproject.toml
|
|
27
|
+
|
|
28
|
+
- name: Install build tooling
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
python -m pip install build
|
|
32
|
+
|
|
33
|
+
- name: Build distributions
|
|
34
|
+
run: python -m build
|
|
35
|
+
|
|
36
|
+
- name: Publish to PyPI
|
|
37
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
|
|
8
|
+
# Virtual environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Tooling caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
site/
|
|
18
|
+
docs_site/
|
|
19
|
+
dist/
|
|
20
|
+
build/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
|
|
23
|
+
# IDE/editor
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
|
|
27
|
+
# OS files
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributing to OSM SDK
|
|
2
|
+
Contributions to OSM SDK are welcome!
|
|
3
|
+
|
|
4
|
+
OSM SDK is a Python SDK for Online Scout Manager (OSM).
|
|
5
|
+
|
|
6
|
+
This guide explains how to contribute to the project.
|
|
7
|
+
|
|
8
|
+
## How to Contribute
|
|
9
|
+
There are several ways to contribute to OSM SDK:
|
|
10
|
+
- **Report bugs:** Find an issue and describe the problem you encountered
|
|
11
|
+
- **Fix bugs:** Submit a pull request with a proposed fix
|
|
12
|
+
- **Request features:** Suggest a new feature or improvement
|
|
13
|
+
- **Improve documentation:** Submit a pull request with improved documentation
|
|
14
|
+
|
|
15
|
+
## Submitting a Pull Request
|
|
16
|
+
To submit a pull request, follow these steps:
|
|
17
|
+
- Fork the repository
|
|
18
|
+
- Create a new branch for your changes
|
|
19
|
+
- Make your changes and write tests if applicable
|
|
20
|
+
- Commit and push your changes to your fork
|
|
21
|
+
- Submit a pull request to the `main` branch of this repository
|
|
22
|
+
|
|
23
|
+
### Pull Request Title Convention
|
|
24
|
+
To maintain a clean and automated changelog, this project requires pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). Titles should be formatted as follows:
|
|
25
|
+
`<type>: <description>`
|
|
26
|
+
|
|
27
|
+
Common types include:
|
|
28
|
+
- `feat`: A new feature
|
|
29
|
+
- `fix`: A bug fix
|
|
30
|
+
- `docs`: Documentation only changes
|
|
31
|
+
- `style`: Changes that do not affect the meaning of the code (white-space, formatting, etc.)
|
|
32
|
+
- `refactor`: A code change that neither fixes a bug nor adds a feature
|
|
33
|
+
- `test`: Adding missing tests or correcting existing tests
|
|
34
|
+
- `chore`: Changes to the build process or auxiliary tools and libraries such as documentation generation
|
|
35
|
+
|
|
36
|
+
## Testing Guidelines
|
|
37
|
+
To ensure that changes work as expected, follow these steps:
|
|
38
|
+
- Use the provided pytest test framework to write tests
|
|
39
|
+
- Write tests for all new features or bug fixes
|
|
40
|
+
- Ensure all tests pass before submitting a pull request
|
|
41
|
+
|
|
42
|
+
## Release Process
|
|
43
|
+
This project uses [setuptools-scm](https://setuptools-scm.readthedocs.io/) for versioning.
|
|
44
|
+
|
|
45
|
+
Versions are automatically determined by Git tags in the format `vMAJOR.MINOR.PATCH`.
|
|
46
|
+
To create a new release, use the GitHub UI to create a new "Release", which will automatically create the required Git tag and trigger the deployment workflow.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
By contributing to OSM SDK, you agree that your contributions will be licensed under the MIT License.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Shephard
|
|
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.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: osm-sdk
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: A Python SDK for Online Scout Manager (OSM)
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: pydantic>=2.0.0
|
|
10
|
+
Requires-Dist: email-validator>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
14
|
+
Requires-Dist: respx>=0.21.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
16
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# OSM SDK
|
|
20
|
+
|
|
21
|
+
OSM SDK is a Python SDK for Online Scout Manager (OSM).
|
|
22
|
+
|
|
23
|
+
The project is provided as a PyPI package that is published on [pypi.org](https://pypi.org/project/osm-sdk/), and it can be installed by running:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install osm-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
OSM SDK requires an OAuth Client ID and Oauth Secret.
|
|
32
|
+
|
|
33
|
+
These can be generated within OSM, by going to Settings → My Account Details → Developer Tools → Create Application.
|
|
34
|
+
|
|
35
|
+
Client Credentials Grant must be enabled for the application.
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from osm_sdk import OSMClient, OSMAttendanceStatus
|
|
41
|
+
|
|
42
|
+
client = OSMClient(client_id="...", client_secret="...")
|
|
43
|
+
|
|
44
|
+
section = client.get_section(name="Scouts")
|
|
45
|
+
term = section.current_term()
|
|
46
|
+
|
|
47
|
+
for member in term.list_members():
|
|
48
|
+
contacts = member.list_contacts()
|
|
49
|
+
print(f"Member: {member.full_name} has {len(contacts)} contacts.")
|
|
50
|
+
|
|
51
|
+
for event in term.list_events():
|
|
52
|
+
attendance = event.list_attendance()
|
|
53
|
+
attending = [a for a in attendance if a.status == OSMAttendanceStatus.YES]
|
|
54
|
+
print(f"Event: {event.name}: {len(attending)} attending (out of {len(attendance)})")
|
|
55
|
+
|
|
56
|
+
for meeting in term.list_meetings():
|
|
57
|
+
attendance = meeting.list_attendance()
|
|
58
|
+
attending = [a for a in attendance if a.status == OSMAttendanceStatus.YES]
|
|
59
|
+
print(f"Meeting: {meeting.name}: {len(attending)} attending (out of {len(attendance)})")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
By default, `OSMClient` requests the scopes:
|
|
63
|
+
`section:member:read section:event:read section:programme:read section:attendance:read`.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
The public API is documented in [PUBLIC_API.md](PUBLIC_API.md).
|
|
68
|
+
|
|
69
|
+
## Contributions
|
|
70
|
+
|
|
71
|
+
Contributions are welcome! Read the [contributing guide](CONTRIBUTING.md) to get started.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# OSM SDK Public API
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [OSMClient](#osmclient)
|
|
5
|
+
- [OSMSection](#osmsection)
|
|
6
|
+
- [OSMTerm](#osmterm)
|
|
7
|
+
- [OSMMember](#osmmember)
|
|
8
|
+
- [OSMContact](#osmcontact)
|
|
9
|
+
- [OSMEvent](#osmevent)
|
|
10
|
+
- [OSMMeeting](#osmmeeting)
|
|
11
|
+
- [OSMAttendance](#osmattendance)
|
|
12
|
+
- [Enums](#enums)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## OSMClient
|
|
17
|
+
|
|
18
|
+
### Methods
|
|
19
|
+
- `get_sections() -> list[OSMSection]`
|
|
20
|
+
- `get_section(name: str | None = None, *, section_id: str | None = None) -> OSMSection`
|
|
21
|
+
- `close() -> None`
|
|
22
|
+
|
|
23
|
+
## OSMSection
|
|
24
|
+
|
|
25
|
+
### Properties
|
|
26
|
+
- `id: str`
|
|
27
|
+
- `name: str`
|
|
28
|
+
|
|
29
|
+
### Methods
|
|
30
|
+
- `list_terms() -> list[OSMTerm]`
|
|
31
|
+
- `current_term() -> OSMTerm`
|
|
32
|
+
- `list_members(*, waiting_list: bool = False) -> list[OSMMember]`
|
|
33
|
+
|
|
34
|
+
## OSMTerm
|
|
35
|
+
|
|
36
|
+
### Properties
|
|
37
|
+
- `id: str`
|
|
38
|
+
- `name: str`
|
|
39
|
+
- `start_date: date`
|
|
40
|
+
- `end_date: date`
|
|
41
|
+
- `current: bool` (computed)
|
|
42
|
+
|
|
43
|
+
### Methods
|
|
44
|
+
- `list_members() -> list[OSMMember]`
|
|
45
|
+
- `list_events() -> list[OSMEvent]`
|
|
46
|
+
- `list_meetings() -> list[OSMMeeting]`
|
|
47
|
+
|
|
48
|
+
## OSMMember
|
|
49
|
+
|
|
50
|
+
### Properties
|
|
51
|
+
- `id: str`
|
|
52
|
+
- `first_name: str`
|
|
53
|
+
- `last_name: str`
|
|
54
|
+
- `type: OSMMemberType`
|
|
55
|
+
- `date_of_birth: date | None`
|
|
56
|
+
- `date_joined_movement: date | None`
|
|
57
|
+
- `gender: str | None`
|
|
58
|
+
- `address: str | None`
|
|
59
|
+
- `postcode: str | None`
|
|
60
|
+
- `primary_email: str | None`
|
|
61
|
+
- `secondary_email: str | None`
|
|
62
|
+
- `primary_phone: str | None`
|
|
63
|
+
- `secondary_phone: str | None`
|
|
64
|
+
- `full_name: str` (computed)
|
|
65
|
+
|
|
66
|
+
### Methods
|
|
67
|
+
- `list_contacts() -> list[OSMContact]`
|
|
68
|
+
|
|
69
|
+
## OSMContact
|
|
70
|
+
|
|
71
|
+
### Properties
|
|
72
|
+
- `type: OSMContactType`
|
|
73
|
+
- `first_name: str | None`
|
|
74
|
+
- `last_name: str | None`
|
|
75
|
+
- `full_name: str` (computed)
|
|
76
|
+
- `relationship: str | None`
|
|
77
|
+
- `primary_email: str | None`
|
|
78
|
+
- `secondary_email: str | None`
|
|
79
|
+
- `primary_phone: str | None`
|
|
80
|
+
- `secondary_phone: str | None`
|
|
81
|
+
- `postcode: str | None`
|
|
82
|
+
- `address: str | None`
|
|
83
|
+
|
|
84
|
+
## OSMEvent
|
|
85
|
+
|
|
86
|
+
### Properties
|
|
87
|
+
- `id: str`
|
|
88
|
+
- `name: str`
|
|
89
|
+
- `start_date: date`
|
|
90
|
+
- `end_date: date`
|
|
91
|
+
|
|
92
|
+
### Methods
|
|
93
|
+
- `list_attendance() -> list[OSMAttendance]`
|
|
94
|
+
|
|
95
|
+
## OSMMeeting
|
|
96
|
+
|
|
97
|
+
### Properties
|
|
98
|
+
- `id: str`
|
|
99
|
+
- `name: str`
|
|
100
|
+
- `date: date`
|
|
101
|
+
|
|
102
|
+
### Methods
|
|
103
|
+
- `list_attendance() -> list[OSMAttendance]`
|
|
104
|
+
|
|
105
|
+
## OSMAttendance
|
|
106
|
+
|
|
107
|
+
### Properties
|
|
108
|
+
- `member: OSMMember`
|
|
109
|
+
- `status: OSMAttendanceStatus`
|
|
110
|
+
|
|
111
|
+
## Enums
|
|
112
|
+
- `OSMAttendanceStatus` (`yes`, `no`, `unknown`)
|
|
113
|
+
- `OSMContactType` (`PRIMARY_CONTACT`, `SECONDARY_CONTACT`, `EMERGENCY_CONTACT`)
|
|
114
|
+
- `OSMMemberType` (`YOUNG_PERSON`, `YOUNG_LEADER`, `LEADER`)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# OSM SDK
|
|
2
|
+
|
|
3
|
+
OSM SDK is a Python SDK for Online Scout Manager (OSM).
|
|
4
|
+
|
|
5
|
+
The project is provided as a PyPI package that is published on [pypi.org](https://pypi.org/project/osm-sdk/), and it can be installed by running:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install osm-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
OSM SDK requires an OAuth Client ID and Oauth Secret.
|
|
14
|
+
|
|
15
|
+
These can be generated within OSM, by going to Settings → My Account Details → Developer Tools → Create Application.
|
|
16
|
+
|
|
17
|
+
Client Credentials Grant must be enabled for the application.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from osm_sdk import OSMClient, OSMAttendanceStatus
|
|
23
|
+
|
|
24
|
+
client = OSMClient(client_id="...", client_secret="...")
|
|
25
|
+
|
|
26
|
+
section = client.get_section(name="Scouts")
|
|
27
|
+
term = section.current_term()
|
|
28
|
+
|
|
29
|
+
for member in term.list_members():
|
|
30
|
+
contacts = member.list_contacts()
|
|
31
|
+
print(f"Member: {member.full_name} has {len(contacts)} contacts.")
|
|
32
|
+
|
|
33
|
+
for event in term.list_events():
|
|
34
|
+
attendance = event.list_attendance()
|
|
35
|
+
attending = [a for a in attendance if a.status == OSMAttendanceStatus.YES]
|
|
36
|
+
print(f"Event: {event.name}: {len(attending)} attending (out of {len(attendance)})")
|
|
37
|
+
|
|
38
|
+
for meeting in term.list_meetings():
|
|
39
|
+
attendance = meeting.list_attendance()
|
|
40
|
+
attending = [a for a in attendance if a.status == OSMAttendanceStatus.YES]
|
|
41
|
+
print(f"Meeting: {meeting.name}: {len(attending)} attending (out of {len(attendance)})")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
By default, `OSMClient` requests the scopes:
|
|
45
|
+
`section:member:read section:event:read section:programme:read section:attendance:read`.
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
The public API is documented in [PUBLIC_API.md](PUBLIC_API.md).
|
|
50
|
+
|
|
51
|
+
## Contributions
|
|
52
|
+
|
|
53
|
+
Contributions are welcome! Read the [contributing guide](CONTRIBUTING.md) to get started.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# OSM SDK: Technical Specification & Implementation Plan (Sync-Only TDD)
|
|
2
|
+
|
|
3
|
+
This document is the authoritative specification for building the `osm-sdk`. Any agent or developer implementing this plan must follow these mandates strictly.
|
|
4
|
+
|
|
5
|
+
## 1. Core Architectural Mandates
|
|
6
|
+
- **Sync-Only Implementation**: To eliminate code duplication and maintainability overhead, the SDK is **Sync-Only**. No async support is required.
|
|
7
|
+
- **Strict Anti-Hallucination**: The developer/agent **MUST NEVER** guess API endpoints, query parameters, or JSON structures. If the information is not provided in the current context, the agent **MUST STOP and ask the developer** for the "Ground Truth" (real JSON samples and endpoint URLs).
|
|
8
|
+
- **Strict TDD**: Development proceeds in a failing test -> implementation -> verification loop. Tests MUST be based on real-world OSM payload samples.
|
|
9
|
+
- **Strict Canonicalization**: All raw data from OSM is mapped into flat, strictly-typed Pydantic models with `extra="forbid"`. This is a hard security requirement to prevent PII leakage (medical, behavioral, etc.).
|
|
10
|
+
- **Person-Centric API**: While the internal data is hierarchical (`Section -> Term -> Member`), the public API must be person-centric. Methods like `member.list_contacts()` must live on the `Member` object.
|
|
11
|
+
- **Transparent Caching**: Tokens and Hierarchy data (`StartupData`) must be cached for the life of the client instance.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Phase 1: Authentication & Hierarchy Discovery
|
|
16
|
+
- **Authenticator**: Implement the `client_credentials` flow with token caching.
|
|
17
|
+
- **Discovery**: Implement the `Startup` fetch to populate the internal `Section` and `Term` cache.
|
|
18
|
+
- **Verification**: A "Smoke Test" that confirms credentials can successfully fetch the hierarchy.
|
|
19
|
+
|
|
20
|
+
## Phase 3: The Public API Surface (Person-Centric)
|
|
21
|
+
Implement the wrapper classes that provide the "Natural" API:
|
|
22
|
+
- `OSMSection.list_terms()` / `OSMSection.current_term()`
|
|
23
|
+
- `OSMTerm.list_members()` (Scoped to that term).
|
|
24
|
+
- `OSMMember.list_contacts()` (Resolves the linkage table to return `List[Contact]`).
|
|
25
|
+
- `OSMEvent.list_attendance()` (Scoped to that event).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Communication Protocol
|
|
30
|
+
- **Ambiguity = Question**: If any detail is missing, ASK.
|
|
31
|
+
- **No Hallucinations**: Never guess a field name. If you haven't seen the JSON, you don't know the field.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from .client import OSMClient
|
|
2
|
+
from .errors import (
|
|
3
|
+
OSMAmbiguousMatchError,
|
|
4
|
+
OSMAPIError,
|
|
5
|
+
OSMAuthenticationError,
|
|
6
|
+
OSMDataError,
|
|
7
|
+
OSMError,
|
|
8
|
+
OSMNotFoundError,
|
|
9
|
+
)
|
|
10
|
+
from .models.attendance import OSMAttendance
|
|
11
|
+
from .models.attendance_status import OSMAttendanceStatus
|
|
12
|
+
from .models.contact import OSMContact
|
|
13
|
+
from .models.contact_type import OSMContactType
|
|
14
|
+
from .models.event import OSMEvent
|
|
15
|
+
from .models.meeting import OSMMeeting
|
|
16
|
+
from .models.member import OSMMember
|
|
17
|
+
from .models.member_type import OSMMemberType
|
|
18
|
+
from .models.section import OSMSection
|
|
19
|
+
from .models.term import OSMTerm
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"OSMClient",
|
|
23
|
+
"OSMAttendance",
|
|
24
|
+
"OSMContact",
|
|
25
|
+
"OSMContactType",
|
|
26
|
+
"OSMEvent",
|
|
27
|
+
"OSMMeeting",
|
|
28
|
+
"OSMMember",
|
|
29
|
+
"OSMMemberType",
|
|
30
|
+
"OSMSection",
|
|
31
|
+
"OSMTerm",
|
|
32
|
+
"OSMAttendanceStatus",
|
|
33
|
+
"OSMError",
|
|
34
|
+
"OSMAuthenticationError",
|
|
35
|
+
"OSMAPIError",
|
|
36
|
+
"OSMDataError",
|
|
37
|
+
"OSMNotFoundError",
|
|
38
|
+
"OSMAmbiguousMatchError",
|
|
39
|
+
]
|
|
40
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .errors import OSMAuthenticationError
|
|
9
|
+
|
|
10
|
+
DEFAULT_TOKEN_URL = "https://www.onlinescoutmanager.co.uk/oauth/token"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Authenticator:
|
|
14
|
+
"""OAuth2 client-credentials authenticator with in-memory token caching."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
client_id: str,
|
|
19
|
+
client_secret: str,
|
|
20
|
+
scope: Optional[str] = None,
|
|
21
|
+
token_url: str = DEFAULT_TOKEN_URL,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._client_id = client_id
|
|
24
|
+
self._client_secret = client_secret
|
|
25
|
+
self._scope = scope
|
|
26
|
+
self._token_url = token_url
|
|
27
|
+
self._cached_token: Optional[str] = None
|
|
28
|
+
self._token_expires_at: Optional[datetime] = None
|
|
29
|
+
|
|
30
|
+
def get_access_token(self, http_client: httpx.Client) -> str:
|
|
31
|
+
if self._cached_token and self._token_is_valid():
|
|
32
|
+
return self._cached_token
|
|
33
|
+
|
|
34
|
+
payload = {"grant_type": "client_credentials"}
|
|
35
|
+
if self._scope:
|
|
36
|
+
payload["scope"] = self._scope
|
|
37
|
+
payload["client_id"] = self._client_id
|
|
38
|
+
payload["client_secret"] = self._client_secret
|
|
39
|
+
|
|
40
|
+
response = http_client.post(
|
|
41
|
+
self._token_url,
|
|
42
|
+
data=payload,
|
|
43
|
+
auth=(self._client_id, self._client_secret),
|
|
44
|
+
headers={"Accept": "application/json"},
|
|
45
|
+
)
|
|
46
|
+
if response.status_code < 200 or response.status_code >= 300:
|
|
47
|
+
raise OSMAuthenticationError(
|
|
48
|
+
f"OSM token request failed ({response.status_code}): {response.text}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
token_payload = response.json()
|
|
52
|
+
access_token = token_payload.get("access_token")
|
|
53
|
+
if not isinstance(access_token, str) or not access_token:
|
|
54
|
+
raise OSMAuthenticationError(
|
|
55
|
+
"OSM token response did not include a valid 'access_token' field."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
self._cached_token = access_token
|
|
59
|
+
self._token_expires_at = self._compute_expiry(token_payload.get("expires_in"))
|
|
60
|
+
return access_token
|
|
61
|
+
|
|
62
|
+
def _token_is_valid(self) -> bool:
|
|
63
|
+
if self._token_expires_at is None:
|
|
64
|
+
return True
|
|
65
|
+
return datetime.now(timezone.utc) < self._token_expires_at
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _compute_expiry(expires_in: object) -> Optional[datetime]:
|
|
69
|
+
if not isinstance(expires_in, int):
|
|
70
|
+
return None
|
|
71
|
+
if expires_in <= 0:
|
|
72
|
+
return None
|
|
73
|
+
return datetime.now(timezone.utc) + timedelta(seconds=max(0, expires_in - 30))
|
|
74
|
+
|