Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
408e685
fix: package dependencies
devraj Dec 4, 2025
8d06445
fix: mcp and cli have runnables
devraj Dec 4, 2025
7bbc414
refactor: tui package mimics cli/mcp
devraj Dec 4, 2025
d58f593
fix: mixed up pyproject
devraj Dec 4, 2025
ddb1e2f
chore: update version to b1
devraj Dec 4, 2025
a19e768
refactor: moves mcp into client and server packages #31
devraj Dec 4, 2025
8eb62f5
fix: package runners
devraj Dec 4, 2025
3499f25
docs: adds mcp docs
devraj Dec 4, 2025
1faebd9
chore: update name of entry script
devraj Dec 5, 2025
f205f8f
fix: missing mkdocs livereload parameter
devraj Dec 5, 2025
9bb2ea9
docs: updates server thought process for mcp
devraj Dec 5, 2025
c7a9d05
chore: adds pydantic-settings #96
devraj Dec 7, 2025
238161d
refactor!: basic syntax rewrite of the core classes
devraj Dec 7, 2025
572e24e
fix: drops calling discover on each method #96
devraj Dec 7, 2025
8b53b09
refactor!: all endpoints now use the new format
devraj Dec 7, 2025
37bb800
fix: reference to constant
devraj Dec 7, 2025
1a308af
refactor: access_groups api client #96
devraj Dec 8, 2025
aeaae5c
refactor: first tests to try out the refactor #96
devraj Dec 8, 2025
2a69aeb
docs: adds example for testing tls
devraj Dec 8, 2025
7bbabd5
fix: drops imports before review of #97
devraj Dec 8, 2025
6e6d7f9
refactor!: access_groups working with the new syntax #96
devraj Dec 8, 2025
9c5236d
reactor: fixes syntax for alarms #96
devraj Dec 8, 2025
88ade5a
refactor: test cases for #96
devraj Dec 8, 2025
950e785
refactor: api endpoints conform to explicit pattern #96
devraj Dec 8, 2025
98dc9ce
reactor: test cases to match endpoint #96
devraj Dec 8, 2025
8d8f685
refactor!: ports most of the endpoints and tests #96
devraj Dec 8, 2025
3b30502
fix: discovery endpoint which doesn't use the api client #96
devraj Dec 8, 2025
6b09a86
fix: almost passing tests #96
devraj Dec 8, 2025
0546728
fix: tests configuration #96
devraj Dec 8, 2025
0eae3e4
refactor: unifies class names #96
devraj Dec 8, 2025
de194e0
refactor: private key to tls key
devraj Dec 8, 2025
3e249ef
docs: starts refactor of docs post #96
devraj Dec 8, 2025
09a0f28
refactor: adds support for basic authentication #65
devraj Dec 8, 2025
ec4e865
refactor: wires up cli #13 post #96
devraj Dec 15, 2025
dafa791
feat: quick and dirty wireup of mcp #70
devraj Dec 15, 2025
023a7a9
refactor: adds choice for output format #35
devraj Dec 21, 2025
f11ca36
docs: update commands to suit new format
devraj Jan 15, 2026
23230fc
fix: package versions
devraj Jan 15, 2026
a50bbf8
fix: update packags
devraj Feb 22, 2026
2333b83
docs: add CLAUDE.md with architecture guide and dev commands
devraj Feb 22, 2026
7a7739d
feat: implement 8 missing REST endpoints with DTOs, tests, and CLI
devraj Mar 7, 2026
c692019
chore: updates packages
devraj Mar 7, 2026
f74d149
fix: move dev into dependency-groups
devraj Mar 8, 2026
9876b91
chore: syncs packages
devraj Mar 8, 2026
594db5a
fix: action install all groups
devraj Mar 8, 2026
105f006
docs: fills in missing pdf docs REFS #1
devraj Mar 16, 2026
8baffd0
chore: update package name
devraj Mar 25, 2026
32e6dd1
fix: follow is mixing and matching between next and updates urls
devraj Mar 25, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Install uv
run: pip install uv
- name: Sync dependencies
run: uv sync
run: uv sync --all-groups
- name: Run tests
run: |
export GACC_API_KEY="${{ secrets.GACC_API_KEY }}"
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Changelog

As of `beta-1` we started using the format outlined by [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).

## Unreleased

### Added

- Complely refactored the structure of the SDK to be `explicit` rather than `static` in how endpoints are defined. See [#96](https://github.com/anomaly/gallagher/issues/96) for discussions and conclusions.

### Changed

### Deprecated

### Removed

### Fixed

### Security
103 changes: 103 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# CLAUDE.md

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

## Commands

This project uses [Taskfile](https://taskfile.dev) with `uv` as the Python package manager. Run all commands from the repo root.

```bash
# Run all tests (hits a live demo Command Centre via the cloud gateway)
task test

# Run a single test file
task test -- test_cardholder.py

# Run a single test by name
task test -- test_cardholder.py::test_cardholder_list

# List available tests
task test:list

# Lint
task lint

# Format
task black

# View coverage report (after running tests)
task test:coverreport

# Run TUI in dev mode
task dev:tui

# Run docs server
task dev:docs

# Run MCP server
task mcp:serve
```

Required environment variables (stored in `.env`, loaded by Taskfile):
- `GACC_API_KEY` — Gallagher Command Centre API key (8-segment format)
- `CERTIFICATE_ANOMALY` — TLS certificate contents (optional, for mTLS)
- `PRIVATE_KEY_ANOMALY` — TLS private key contents (optional, for mTLS)

Tests run against a live demo Command Centre. The `conftest.py` fixture randomly alternates between `GGL-API-KEY` and Basic authentication on each test run (since both are supported).

## Architecture

The toolkit is structured as five distinct layers that all build on the Python SDK:

```
gallagher/
cc/ # Python SDK — API client and endpoint definitions
dto/ # Data Transfer Objects (Pydantic models)
cli/ # CLI (Typer-based, entry point: `gala`)
tui/ # TUI (Textual-based, entry point: `gtui`)
mcp/ # MCP server (FastMCP, entry point: `gala-mcp-server`)
ext/ # Extensions (shillelagh SQL adapter)
```

### SDK layer (`gallagher/cc/`)

`APIClient` in `gallagher/cc/__init__.py` is the main entry point. On init it performs HATEOAS discovery by calling the root API endpoint, then instantiates each resource endpoint (e.g. `self.cardholders`, `self.doors`) passing the `CommandCentreConfig` and the `DiscoveryResponse`.

`CommandCentreConfig` (in `gallagher/cc/core.py`) is a `pydantic-settings` class that reads config from env vars or explicit arguments. Key fields: `api_key`, `api_base` (defaults to the AU cloud gateway), `file_tls_certificate`, `file_tls_key`, `use_basic_authentication`, `proxy`.

All resource endpoints inherit from `APIEndpoint` (in `gallagher/cc/core.py`) and implement `get_config() -> EndpointConfig`. `EndpointConfig` maps the HATEOAS-discovered hrefs to Pydantic DTO classes for list, retrieve, and follow operations. The base class provides `list()`, `retrieve()`, `search()`, `follow()`, `next()`, `previous()`, `_get()`, and `_post()` methods.

The `follow()` method implements Gallagher's long-poll pattern (30s server timeout, follow `next` href in a loop, stop via `asyncio.Event`).

### DTO layer (`gallagher/dto/`)

DTOs follow a strict hierarchy:
- **`Ref`** — minimal `href` + optional metadata (e.g. `name`)
- **`Summary`** — subset of attributes returned in list responses
- **`Detail`** — full object (extends Summary)
- **`Response`** — wraps a list of Summaries with pagination hrefs (`next`, `previous`, `updates`)
- **`Payload`** — used for POST/PATCH request bodies

All DTO classes inherit from `AppBaseModel` (in `gallagher/dto/utils.py`) which configures Pydantic to auto-translate between camelCase (API) and snake_case (Python). Core mixins: `HrefMixin`, `OptionalHrefMixin`, `IdentityMixin`, `OptionalIdentityMixin`.

`AppBaseResponseWithFollowModel` is the base for paginated responses and adds `next`, `previous`, and `updates` optional hrefs.

### CLI layer (`gallagher/cli/`)

Built with Typer. `gallagher/cli/__init__.py` wires the global callback (which initialises `APIClient`) and registers all sub-commands. Global options are passed via environment variables or CLI flags: `GACC_API_KEY`, `GACC_TLS_CERT`, `GACC_TLS_KEY`, `GACC_GATEWAY` (AU/US), `GACC_PROXY_URL`, `GACC_USE_BASIC_AUTH`. The `--format` flag supports `pretty`, `json`, `csv`, `markdown`. The `APIClient` instance is passed to subcommands via Typer's `click.Context`.

### MCP layer (`gallagher/mcp/`)

Implemented with FastMCP. The server (`gallagher/mcp/server/__init__.py`) reads credentials from environment variables, writes TLS material to temp files, and registers `@mcp.tool()` decorated async functions that call `api_client.*` methods.

### SQL extension (`gallagher/ext/shillelagh/`)

Provides a [shillelagh](https://github.com/betodealmeida/shillelagh) adapter (`CCAPIAdapter`) that lets SQL clients query API endpoints as virtual tables. Endpoints opt-in to SQL exposure via `__shillelagh__ = (EndpointClass,)` at the bottom of their module. Column types are derived dynamically from DTO annotations.

## Key conventions

- **Never hardcode URLs.** All endpoint hrefs come from HATEOAS discovery (`DiscoveryResponse`). The only hardcoded URLs are in `gallagher/const.py` (the gateway base URLs).
- **All endpoint calls are async** (`await api_client.cardholders.list()`). The CLI uses `AsyncTyper` to bridge Typer's sync interface.
- **Exceptions** in `gallagher/exception.py`: `UnlicensedFeatureException` (HTTP 403), `NotFoundException` (HTTP 404), `AuthenticationError` (HTTP 401), `DeadEndException` (no next/previous href), `PathFollowNotSupportedError`.
- **`__shillelagh__`** tuple at module level in endpoint files lists classes exposed as SQL virtual tables.
- **`pytest-asyncio`** is configured with `asyncio_mode = auto` so all async test functions are run automatically without decorators.
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Our Python Toolkit focuses on enhancing the developer experience (DX) around the
- **Command Line Interface** (CLI) to build powerful pipeline-based workflows.
- **Terminal User Interface** (TUI) for easy interactions with the Command Centre.
- **SQL interface** query the REST API as if it were a database or interact with via an ORM.
- **MCP Server and Client** implement the [Model Context Protocol](https://modelcontextprotocol.io) to allow LLMs to interact with Gallagher Command Centre.

> [!NOTE]\
> This project is **NOT** affiliated with Gallagher Security. All trademarks are the property of their respective owners.
Expand All @@ -40,20 +41,28 @@ import os
import asyncio

# Import the client and models
from gallagher import cc
from gallagher.cc import CommandCentreConfig, APIClient
from gallagher.dto.summary import CardholderSummary
from gallagher.cc.cardholders import Cardholder

# Optionally provide a client certificate and key
cert_path = os.path.join(os.getcwd(), "client.pem")
key_path = os.path.join(os.getcwd(), "client.key")

# Set the API key from the environment
api_key = os.environ.get("GACC_API_KEY")
cc.api_key = api_key

# Make a configuration object
config = CommandCentreConfig(
api_key=api_key,
file_tls_cert=cert_path,
file_tls_key=key_path,
)

# Initialise the client
client = APIClient(config=config)

# Async support gives us back a coroutine
ch_coro = Cardholder.list()
ch_coro = client.cardholders.list()

# Run the coroutine to get the cardholder
cardholders = asyncio.run(ch_coro)
Expand Down
8 changes: 6 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ tasks:

this does not run coverage or provide tap output
cmds:
- uv run coverage run -m pytest -s tests/{{.CLI_ARGS}}
- uv run coverage run -m pytest -s tests/{{.CLI_ARGS}} -v
test:tap:
desc: runs tests with tap output
summary: runs all the tests inside the virtualenv, optionally
Expand Down Expand Up @@ -63,7 +63,11 @@ tasks:
dev:docs:
desc: run the mkdocs server with appropriate flags
cmds:
- cd docs && uv run -m mkdocs serve --open -a localhost:8001
- cd docs && uv run -m mkdocs serve --livereload --watch-theme --open -a localhost:8001
mcp:serve:
desc: runs the mcp server for development
cmds:
- uv run gala-mcp-server --dev --reload --host
debug:get:
desc: use httpie to get payload from CC
summary: |
Expand Down
41 changes: 38 additions & 3 deletions docs/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,46 @@

We provide a command line interface to interact with the Gallagher Command Centre. It uses the API client to communicate with the server, which doubly serves as a reference example of how to use the API client.

We use [typer](https://typer.tiangolo.com) to construct the CLI, which in turn uses [click](https://click.palletsprojects.com). We also use [rich](https://rich.readthedocs.io/en/stable/) to make the output nicer. The CLI is decoupled from the API client, and is not install by default.
The CLI builds on [typer](https://typer.tiangolo.com) which in turn uses [click](https://click.palletsprojects.com). It also uses [rich](https://rich.readthedocs.io/en/stable/) to make the output nicer. The CLI is decoupled from the API client, and is not install by default.

We follow a `git` like `command`, `sub-command` pattern, so it should feel quite familiar.
The design follows the `git` like `command`, `sub-command` pattern, so it should feel quite familiar.

uv will install the alias `gal` for you to interact with the CLI. You can ask for help with:
## Installation

If you are installing the SDK then `uv` will install the alias `gala` for you to interact with the CLI.

You can also use `pipx` to install the CLI directly from PyPI:

```bash
pipx install gallagher-sdk
```

## Usage

There are a few things to keep in mind while using the CLI, if you are lost you can always as for help:

```bash
gala --help
```

You can pass the configuration variables as command line options or as environment variables. The command line options take precedence over the environment variables.

```bash
gala --api-key YOUR_API_KEY --gateway US ch find devraj
```

alternatively if you had the environment variable `GACC_API_KEY` set you could do:

```bash
gala ch find devraj
```

> Remember: that the global command line options must precede the command and sub-command.


## CLI reference

The following is an auto-generated reference for the CLI commands and options and is kept upto date within the code itself.

::: mkdocs-typer2
:module: gallagher.cli
Expand Down
6 changes: 2 additions & 4 deletions docs/docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ class Alarms(
""" Alarms
"""

@classmethod
async def get_config(cls) -> EndpointConfig:
def get_config(self) -> EndpointConfig:
return EndpointConfig(
endpoint=Capabilities.CURRENT.features.alarms.alarms,
dto_list=AlarmSummaryResponse,
Expand Down Expand Up @@ -229,8 +228,7 @@ class Division(APIEndpoint):

"""

@classmethod
async def get_config(cls) -> EndpointConfig:
def get_config(self) -> EndpointConfig:
return EndpointConfig(
endpoint=Capabilities.CURRENT.features.divisions.divisions,
dto_list=DivisionSummaryResponse,
Expand Down
1 change: 1 addition & 0 deletions docs/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Our Python Toolkit focuses on enhancing the developer experience (DX) around the
- **Command Line Interface** (CLI) to build powerful pipeline-based workflows.
- **Terminal User Interface** (TUI) for easy interactions with the Command Centre.
- **SQL interface** query the REST API as if it were a database or interact with via an ORM.
- **MCP Server** which implements the [Model Context Protocol](https://modelcontextprotocol.io) to allow LLMs to interact with Gallagher Command Centre (we do provide a simple client for testing).

!!! note

Expand Down
38 changes: 29 additions & 9 deletions docs/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ If you are feeling adventurous you can install everything by:
uv add gallagher[all]
```

In case you find your setup missing packages, try syncing with `--all-extras` flag (probably relevant if you are using all the tools or developing with the SDK):

```bash
uv sync --all-extras
```

### SDK

To use the API (or the associated tools, as they use the API client in return) you must have an API key supplied by the Command Centre instance. The same key is used if you were were using the API on premise or in the cloud.
Expand All @@ -48,7 +54,8 @@ Command Centre optionally allows you to use self signed client side TLS certific
You can use `openssl` to generate yourself a client side certificate and key.

```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout client.key -out client.pem
openssl req -x509 -nodes -days 365 -newkey rsa:2048\
-keyout client.key -out client.pem
```

Fill in the required details for the certificate and then generate a `sha1` hash of the certificate.
Expand All @@ -67,7 +74,7 @@ api_key = os.environ.get("GACC_API_KEY")
cc.api_key = api_key

cc.file_tls_certificate = '/path/to/client.pem'
cc.file_private_key = '/path/to/client.key'
cc.file_tls_key = '/path/to/client.key'
```

The rest of the requests and operations remain the same, the library will use an `SSL Context` to do the needful.
Expand All @@ -89,7 +96,7 @@ temp_file_certificate = tempfile.NamedTemporaryFile(
suffix=".crt",
delete=False
)
temp_file_private_key = tempfile.NamedTemporaryFile(
temp_file_tls_key = tempfile.NamedTemporaryFile(
suffix=".key",
delete=False
)
Expand All @@ -98,20 +105,33 @@ temp_file_private_key = tempfile.NamedTemporaryFile(
if certificate_anomaly and temp_file_certificate:
temp_file_certificate.write(certificate_anomaly.encode('utf-8'))

if private_key_anomaly and temp_file_private_key:
temp_file_private_key.write(private_key_anomaly.encode('utf-8'))
if private_key_anomaly and temp_file_tls_key:
temp_file_tls_key.write(private_key_anomaly.encode('utf-8'))
```

You can assign these temporary files to the client as shown above.

```python
from gallagher import cc
from gallagher.cc import CommandCentreConfig, APIClient

cc.api_key = api_key
cc.file_tls_certificate = temp_file_certificate.name
cc.file_private_key = temp_file_private_key.name
# Set the API key from the environment
api_key = os.environ.get("GACC_API_KEY")

# Make a configuration object
config = CommandCentreConfig(
api_key=api_key,
file_tls_cert=cert_path, # required if CC demands it
file_tls_key=key_path, # required if above is enabled
)

# Initialise the client
client = APIClient(config=config)
```

> If you want to validate your certificate can use something like `httpie` as demonstrate here
>
> `http --cert=.certs/anomaly-gallagher.pem --cert-key=.certs/anomaly-gallagher.key get https://commandcentre-api-au.security.gallagher.cloud/api/ "Authorization: GGL-API-KEY $GACC_API_KEY"`

### Command line interface

### Terminal user interface
Expand Down
Loading
Loading