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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv venv --python 3.12
- run: uv pip install -e ".[dev]"
- run: uv run ruff check .

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv venv --python ${{ matrix.python-version }}
- run: uv pip install -e ".[dev]"
- run: uv run pytest -v
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Publish to PyPI

on:
push:
tags: ["v*"]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv venv --python 3.12
- run: uv pip install -e ".[dev]"
- run: uv run ruff check .
- run: uv run pytest -v
- run: uv build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/fabric-comanage-api
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,4 @@ cython_debug/
.idea
tldr.py
userinfo
development_plan.md
69 changes: 69 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Changelog

All notable changes to `fabric-comanage-api` are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2026-04-25

A substantial refactor focused on robustness, testability, and packaging
modernization. The public API is fully preserved — all existing methods retain
their names, signatures, and return types.

### Added

- Configurable HTTP request `timeout` parameter on `ComanageApi` (default: 30s).
- Automatic retry with exponential backoff on transient failures (HTTP 429, 500,
502, 503, 504) via `urllib3.util.Retry` mounted on an `HTTPAdapter`. Retries
are applied to all HTTP methods.
- Structured logging under the `comanage_api` logger. A `NullHandler` is
registered so callers who don't configure logging see no output; callers who
enable logging get DEBUG (request URL/method/params), INFO (success), and
WARNING (non-2xx before raise) messages. Credentials and request/response
bodies are never logged.
- 138 unit tests across 10 files in `tests/`, using `pytest` + `requests-mock`,
covering all 9 endpoint modules plus the core `ComanageApi` class. Tests
exercise success paths, parameter validation, HTTP error propagation, and
edge cases (deduplication, 204 empty responses, `parent_id=0`).
- GitHub Actions CI workflow (`.github/workflows/ci.yml`) running `ruff` lint
on Python 3.12 and `pytest` across Python 3.9, 3.10, 3.11, and 3.12.
- `ruff` configuration in `pyproject.toml` (line-length 120, rules E/F/I/W).
- Centralized HTTP helpers on `ComanageApi`: `_get`, `_post`, `_put`, `_delete`,
and `_get_by_entity` — every endpoint module now delegates to these.

### Changed

- **Refactored to mixin architecture.** Each `_*.py` module now exports a mixin
class (e.g. `CoPeopleMixin`, `COUsMixin`, `SshKeysMixin`) and `ComanageApi`
inherits from all 9. The 60-method passthrough wrapper layer in `__init__.py`
is gone.
- Migrated all packaging metadata to `pyproject.toml` (PEP 621). Removed
`setup.cfg`, `requirements.txt`, and `MANIFEST.in`.
- Bumped minimum supported Python from 3.6 to 3.9.
- Adopted `uv` as the recommended package manager for development.
- Invalid enum values now raise `ValueError` instead of `TypeError` across
`_copersonroles`, `_emailaddresses`, `_identifiers`, `_names`, `_sshkeys`,
and `_coorgidentitylinks`.
- Replaced four copies of the validate-and-GET pattern in `view_per_*` methods
with a single `_get_by_entity` helper.

### Fixed

- `cous_edit()` now correctly distinguishes the three `parent_id` cases —
*value provided* (set), *0* (clear parent), *None* (keep existing) — using
explicit `is not None` checks.
- `ssh_keys_add()` no longer stringifies a missing comment as the literal
`"None"`; it now sends an empty string when no comment is supplied.
- `org_identities_view_all()` docstring corrected (previously a copy-paste of
the EmailAddresses docstring).

### Removed

- `requests-mock` removed from runtime `install_requires` (it remains in dev
dependencies). The `_MOCK_501_URL` / `_mock_session` machinery in
`__init__.py` is gone; unimplemented endpoints now raise `NotImplementedError`.

## [0.1.5]

Prior releases — see git history.
71 changes: 71 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

**fabric-comanage-api** — a Python 3 client wrapper for the [COmanage REST API v1](https://spaces.at.internet2.edu/display/COmanage/REST+API+v1), published to PyPI as `fabric-comanage-api` (current version 0.2.0). Part of the [FABRIC Testbed](https://github.com/fabric-testbed) project. MIT licensed.

## Build & Install

```bash
# Development setup with uv
uv venv --python 3.12
uv pip install -e ".[dev]"

# Install from PyPI
pip install fabric-comanage-api
```

Build is configured via `pyproject.toml` (PEP 621 metadata, setuptools backend). Version is sourced from `comanage_api.__VERSION__`. Requires Python >= 3.9.

## Dependencies

Runtime: `requests>=2.25.0`. Dev: `pytest`, `requests-mock`, `python-dotenv` (installed via `pip install -e ".[dev]"`).

## Testing

Unit tests use `pytest` + `requests-mock`. Run with:

```bash
uv run pytest -v
```

138 tests across 10 files in `tests/` cover all endpoint modules: successful responses, parameter validation (`ValueError`), HTTP error propagation, and edge cases (deduplication, 204 empty, `parent_id=0`).

The `examples/` directory contains per-endpoint scripts that exercise the API against a live COmanage instance. To run them, copy `template.env` to `.env`, fill in credentials, then run individual example scripts (e.g., `uv run python examples/cous_example.py`).

## Architecture

`ComanageApi` (in `comanage_api/__init__.py`) is the single public class. It holds connection state (`_CO_API_URL`, `_CO_API_USER`, `_CO_API_PASS`, `_CO_API_ORG_ID`, `_CO_API_ORG_NAME`) and a `requests.Session` for HTTP Basic Auth.

**HTTP helpers** on `ComanageApi` (`_get`, `_post`, `_put`, `_delete`, `_get_by_entity`) centralize all request/response handling. Each API domain module delegates to these helpers.

Each API domain lives in its own private module (`_copeople.py`, `_cous.py`, `_sshkeys.py`, etc.). These modules define standalone functions that accept a `ComanageApi` instance as `self`. The `__init__.py` imports these functions and wraps them as methods on `ComanageApi`, creating a facade.

**API endpoint modules:**
- `_coorgidentitylinks.py` — CoOrgIdentityLink (requires COmanage v4.0.0+)
- `_copeople.py` — CoPerson
- `_copersonroles.py` — CoPersonRole
- `_cous.py` — COU (Collaborative Organizational Unit)
- `_emailaddresses.py` — EmailAddress
- `_identifiers.py` — Identifier
- `_names.py` — Name
- `_orgidentities.py` — OrgIdentity
- `_sshkeys.py` — SshKey (requires COmanage v4.0.0+, experimental)

**Pattern for each module:** functions named `<resource>_<action>` (e.g., `cous_add`, `cous_view_all`, `cous_delete`) that build paths/params/bodies and delegate to `self._get()`, `self._post()`, `self._put()`, or `self._delete()`. Unimplemented endpoints raise `NotImplementedError`.

## Configuration

Environment variables (see `template.env`):
- `COMANAGE_API_USER` / `COMANAGE_API_PASS` — API credentials
- `COMANAGE_API_CO_NAME` / `COMANAGE_API_CO_ID` — target CO
- `COMANAGE_API_URL` — registry base URL
- `COMANAGE_API_SSH_KEY_AUTHENTICATOR_ID` — optional SSH key authenticator plugin ID

## Conventions

- Instance-level constants for valid option sets: `STATUS_OPTIONS`, `AFFILIATION_OPTIONS`, `SSH_KEY_OPTIONS`, `ENTITY_OPTIONS`, `PERSON_OPTIONS`, `EMAILADDRESS_OPTIONS`.
- All API methods validate parameters against these option sets before making HTTP calls.
- Commit messages reference GitHub issues with `[#N]` prefix.
1 change: 0 additions & 1 deletion MANIFEST.in

This file was deleted.

59 changes: 34 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,23 @@ api = ComanageApi(
co_api_pass=COMANAGE_API_PASS,
co_api_org_id=COMANAGE_API_CO_ID,
co_api_org_name=COMANAGE_API_CO_NAME,
co_ssh_key_authenticator_id=COMANAGE_API_SSH_KEY_AUTHENTICATOR_ID
co_ssh_key_authenticator_id=COMANAGE_API_SSH_KEY_AUTHENTICATOR_ID,
timeout=30 # optional, HTTP request timeout in seconds (default: 30)
)
```

**Built-in robustness:** All HTTP requests include a configurable timeout (default 30s) and automatic retry with exponential backoff on transient failures (429, 500, 502, 503, 504).

**Logging:** The library uses Python's standard `logging` module under the `comanage_api` logger. No output is produced by default. To enable:

```python
import logging
logging.basicConfig(level=logging.DEBUG)
```

Get some data! (example using `cous_view_per_co()` which retrieves all COUs attached to a given CO)

```python
$ python
Python 3.9.6 (v3.9.6:db3ff76da1, Jun 28 2021, 11:49:53)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from comanage_api import ComanageApi
>>>
>>> api = ComanageApi(
Expand All @@ -62,7 +67,8 @@ Type "help", "copyright", "credits" or "license" for more information.
... co_api_pass='xxxx-xxxx-xxxx-xxxx',
... co_api_org_id='123',
... co_api_org_name='RegistryName',
... co_ssh_key_authenticator_id='123'
... co_ssh_key_authenticator_id='123',
... timeout=30
... )
>>>
>>> cous = api.cous_view_per_co()
Expand Down Expand Up @@ -117,8 +123,8 @@ Return types based on implementation status of wrapped API endpoints
- `-> dict`: Data is returned as a Python [Dictionary](https://docs.python.org/3/c-api/dict.html) object
- `-> bool`: Success/Failure is returned as Python [Boolean](https://docs.python.org/3/c-api/bool.html) object
- Not Implemented (`### NOT IMPLEMENTED ###`):
- `-> dict`: raise exception (`HTTPError - 501 Server Error: Not Implemented for url: mock://not_implemented_501.local`)
- `-> bool`: raise exception (`HTTPError - 501 Server Error: Not Implemented for url: mock://not_implemented_501.local`)
- `-> dict`: raises `NotImplementedError`
- `-> bool`: raises `NotImplementedError`

### <a name="coorgidentitylink"></a>[CoOrgIdentityLink API](https://spaces.at.internet2.edu/display/COmanage/CoOrgIdentityLink+API) (COmanage v4.0.0+)

Expand All @@ -135,15 +141,15 @@ Return types based on implementation status of wrapped API endpoints
- Edit an existing CO Identity Link.
- `coorg_identity_links_view_all() -> dict`
- Retrieve all existing CO Identity Links.
- `coorg_identity_links_view_by_identity(identifier_id: int) -> dict`
- `coorg_identity_links_view_by_identity(identity_type: str, identity_id: int) -> dict`
- Retrieve all existing CO Identity Links for a CO Person or an Org Identity.
- `coorg_identity_links_view_one(org_identity_id: int) -> dict`
- `coorg_identity_links_view_one(coorg_identity_link_id: int) -> dict`
- Retrieve an existing CO Identity Link.

**NOTE**: when provided, valid values for `identity_type` as follows:

```python
IDENTITY_OPTIONS = ['copersonid', 'orgidentityid']
PERSON_OPTIONS = ['copersonid', 'orgidentityid']
```

### <a name="coperson"></a>[CoPerson API](https://spaces.at.internet2.edu/display/COmanage/CoPerson+API) (COmanage v3.3.0+)
Expand Down Expand Up @@ -311,7 +317,7 @@ PERSON_OPTIONS = ['copersonid', 'orgidentityid']
- Edit an existing Organizational Identity.
- `org_identities_view_all() -> dict`
- Retrieve all existing Organizational Identities.
- `org_identities_view_per_co(person_type: str, person_id: int) -> dict`
- `org_identities_view_per_co() -> dict`
- Retrieve all existing Organizational Identities for the specified CO.
- `org_identities_view_per_identifier(identifier_id: int) -> dict`
- Retrieve all existing Organizational Identities attached to the specified identifier.
Expand All @@ -329,7 +335,7 @@ PERSON_OPTIONS = ['copersonid', 'orgidentityid']
- `ssh_keys_delete(ssh_key_id: int) -> bool`
- Remove an SSH Key.
- `ssh_keys_edit(ssh_key_id: int, coperson_id: int = None, ssh_key: str = None, key_type: str = None, comment: str = None, ssh_key_authenticator_id: int = None) -> bool`
- Edit an exiting SSH Key.
- Edit an existing SSH Key.
- `ssh_keys_view_all() -> dict`
- Retrieve all existing SSH Keys.
- `ssh_keys_view_per_coperson(coperson_id: int) -> dict`
Expand All @@ -346,29 +352,32 @@ SSH_KEY_OPTIONS = ['ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384',

## <a name="usage"></a>Usage

Set up a virtual environment (`virtualenv` is used in these examples)
### Install

Install from PyPI:

```console
virtualenv -p /usr/local/bin/python3 venv
source venv/bin/activate
pip install fabric-comanage-api
```

### Install supporting packages
### Development setup

Install from PyPi
This project uses [uv](https://docs.astral.sh/uv/) for dependency management. To set up a development environment:

```console
pip install fabric-comanage-api
uv venv --python 3.12
uv pip install -e ".[dev]"
```

**OR**

Install for Local Development
### Lint and test

```console
pip install -r requirements.txt
uv run ruff check . # lint
uv run pytest -v # test
```

CI runs both on every push to `main`/`develop` and on PRs, across Python 3.9–3.12.

### Configure your environment

Create a `.env` file from the included template if you don't want to put the API credentials in your code. Example code makes use of [python-dotenv](https://pypi.org/project/python-dotenv/)
Expand Down Expand Up @@ -457,6 +466,6 @@ Pressing the "Edit" option will display the fields for the Authenticator along w
- Identifier API: [https://spaces.at.internet2.edu/display/COmanage/Identifier+API](https://spaces.at.internet2.edu/display/COmanage/Identifier+API)
- Name API: [https://spaces.at.internet2.edu/display/COmanage/Name+API](https://spaces.at.internet2.edu/display/COmanage/Name+API)
- OrgIdentity API: [https://spaces.at.internet2.edu/display/COmanage/OrgIdentity+API](https://spaces.at.internet2.edu/display/COmanage/OrgIdentity+API)
- SsHKey API: [https://spaces.at.internet2.edu/display/COmanage/SshKey+API](https://spaces.at.internet2.edu/display/COmanage/SshKey+API)
- SshKey API: [https://spaces.at.internet2.edu/display/COmanage/SshKey+API](https://spaces.at.internet2.edu/display/COmanage/SshKey+API)
- SSH Key Authenticator Plugin: [https://spaces.at.internet2.edu/display/COmanage/SSH+Key+Authenticator+Plugin](https://spaces.at.internet2.edu/display/COmanage/SSH+Key+Authenticator+Plugin)
- PyPi: [https://pypi.org](https://pypi.org)
Loading
Loading