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
2 changes: 1 addition & 1 deletion .cratis/ai.manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"SourceRevision": "cc9c6312a748913c90d20c4c91b9ebd2e83edfb0",
"SourceRevision": "fc8b4e5e2953befb7ff4b80978ae5997bc77a010",
"Files": [
{
"Source": "agents/backend-developer.md",
Expand Down
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Thank you for helping build the Python client for Chronicle.

## Local setup

You need Python 3.10 or newer and Git. The install downloads the generated contracts wheel from a Chronicle GitHub
release, so it needs network access to GitHub release assets. Docker is needed only for work that talks to a local
Chronicle kernel; see the [client development guide](Documentation/client-development-guide.md#local-kernel).

```shell
python -m venv .venv
source .venv/bin/activate
Expand All @@ -34,6 +38,23 @@ python -m build
python -m twine check dist/*
```

`rm -rf dist` clears distributions from earlier builds so that Twine checks only the current build. In Windows
PowerShell, use `Remove-Item -Recurse -Force dist` instead.

A passing run looks like this:

| Check | Success signal |
| --- | --- |
| `ruff format --check .` | `… files already formatted` |
| `ruff check .` | `All checks passed!` |
| `mypy src` | `Success: no issues found` |
| `pytest` | All tests pass, followed by a coverage report |
| `python -m build` | `Successfully built` one `.tar.gz` and one `.whl` |
| `python -m twine check dist/*` | `PASSED` for both files |

CI runs these checks on Python 3.10 through 3.14. Pull requests that change Markdown also run markdownlint with
the repository's `.markdownlint.json`.

All checks must pass before review. New behavior requires tests, including failure behavior where applicable.

## API principles
Expand Down
148 changes: 135 additions & 13 deletions Documentation/client-development-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,31 @@ Chronicle C# contracts

### Temporary contracts distribution

Until PyPI trusted publishing is configured, the project dependency resolves the verified
`cratis-chronicle-contracts` is not published to PyPI. The project dependency resolves the verified
`cratis-chronicle-contracts` 16.38.2 wheel from its matching
[Chronicle GitHub release](https://github.com/Cratis/Chronicle/releases/tag/v16.38.2). A normal development install
fetches it automatically. Do not copy generated contracts into this repository. The dependency will move to the
PyPI release after [the publisher setup](https://github.com/Cratis/Chronicle.Python/issues/15) and
[first publication](https://github.com/Cratis/Chronicle.Python/issues/16) are complete.
[Chronicle GitHub release](https://github.com/Cratis/Chronicle/releases/tag/v16.38.2), pinned by SHA-256 in
`pyproject.toml`. A normal development install fetches it automatically, so the install needs network access to
GitHub release assets. Do not copy generated contracts into this repository.

The PyPI trusted-publishing setup ([#15](https://github.com/Cratis/Chronicle.Python/issues/15)) and first
publication ([#16](https://github.com/Cratis/Chronicle.Python/issues/16)) were closed as not planned, so there is
no scheduled move to PyPI. The contracts wheel is generated from the v16.38.2 kernel; see
[Kernel version](#kernel-version) before testing against a newer kernel.

## Local kernel

The development kernel listens on `localhost:35000`. Its main listener uses TLS and serves gRPC over HTTP/2. A
development build creates a self-signed certificate and built-in client credentials.
The development kernel listens on port `35000`. Its single listener uses TLS and serves gRPC over HTTP/2, the OAuth
token endpoint, and the Workbench. The development image generates a self-signed certificate at startup and accepts
built-in development client credentials.

Start the development image:
Start the development image that matches the contracts version, bound to the loopback interface only:

```shell
docker run --rm -p 35000:35000 cratis/chronicle:latest-development
docker run --rm -p 127.0.0.1:35000:35000 cratis/chronicle:16.38.2-development
```

The development image embeds MongoDB inside the container, so every event disappears when the container stops.

Use the explicit local-development connection string:

```text
Expand All @@ -48,6 +55,13 @@ chronicle://chronicle-dev-client:chronicle-dev-secret@localhost:35000
These credentials are development defaults only. They are not production credentials or a production
configuration contract.

### Kernel version

The contracts and the probe below were exercised against `cratis/chronicle:16.38.2-development`. Newer kernels,
including `latest-development`, may add or change contracts; compatibility between the 16.38.2 contracts and a
later kernel has not been verified. Name the exact kernel image in any issue, test, or pull request that exercises
network behavior.

## Authentication contract

Request a token from:
Expand All @@ -64,17 +78,106 @@ client_id=<client id>
client_secret=<client secret>
```

Parse `access_token` from the JSON response. Attach the current token to each new gRPC call as metadata:
Parse `access_token` and `expires_in` (seconds) from the JSON response. Attach the current token to each new gRPC
call as metadata:

```text
authorization: Bearer <access token>
```

Token acquisition, caching, expiry, refresh, and call interception should remain separate from the channel. Do
not permanently bake one expiring token into channel headers.
not permanently bake one expiring token into channel headers. Chronicle's
[authentication and bearer tokens](https://www.cratis.io/chronicle/building-a-client/authentication-and-bearer-tokens/)
page describes the behavior the other clients implement: the three authentication modes selected by the connection
string, proactive refresh before expiry, and one retry after an `UNAUTHENTICATED` response.

### TLS and certificate validation

The development kernel's self-signed certificate names `localhost`, `chronicle`, `127.0.0.1`, and `::1`. A Python
gRPC channel rejects it unless the certificate is supplied as a trusted root. A development option may explicitly
relax certificate verification for localhost. Production behavior must retain normal certificate and hostname
validation.

Chronicle's .NET client differs: it accepts any server certificate unless validation is turned on; see
[TLS configuration](https://www.cratis.io/chronicle/configuration/tls/). This guide does not adopt that default.
How the Python client exposes local-development relaxation (an explicit option, a `skipTlsValidation`
connection-string parameter, or both) is a public API decision for
[connection-string parsing](https://github.com/Cratis/Chronicle.Python/issues/2). Do not decide it implicitly in
an implementation, and do not make relaxed validation the behavior for non-local connections. The connection-string grammar, including `skipTlsValidation`, `apiKey`, and `auth=none`, is in
[connection string elements](https://www.cratis.io/chronicle/building-a-client/connection-string-elements/).

### Check the kernel before writing client code

Confirm that the kernel issues tokens before debugging a client. With the kernel from [Local kernel](#local-kernel)
running:

```shell
curl --insecure https://localhost:35000/connect/token \
-d grant_type=client_credentials \
-d client_id=chronicle-dev-client \
-d client_secret=chronicle-dev-secret
```

`--insecure` skips certificate validation and is only acceptable against this local development kernel. A working
kernel returns JSON with `access_token`, `token_type` (`Bearer`), and `expires_in`. A wrong secret returns HTTP 401.

The following probe exercises the same path through the generated contracts: it trusts the kernel's own
certificate, obtains a token, and calls `EnsureEventStore` once without and once with the bearer token. It is a
contributor diagnostic built on the internal contracts package, not the planned public client API.

```python
import asyncio
import json
import ssl
import urllib.parse
import urllib.request

import grpc
from cratis_chronicle_contracts.eventstores_pb2 import EnsureEventStoreRequest
from cratis_chronicle_contracts.eventstores_pb2_grpc import EventStoresStub

HOST, PORT = "localhost", 35000

# Development only: trust whatever certificate the local kernel presents.
certificate = ssl.get_server_certificate((HOST, PORT))
body = urllib.parse.urlencode(
{
"grant_type": "client_credentials",
"client_id": "chronicle-dev-client",
"client_secret": "chronicle-dev-secret",
}
).encode()
request = urllib.request.Request(f"https://{HOST}:{PORT}/connect/token", data=body)
with urllib.request.urlopen(request, context=ssl.create_default_context(cadata=certificate)) as response:
token = json.load(response)["access_token"]


async def main() -> None:
credentials = grpc.ssl_channel_credentials(root_certificates=certificate.encode())
async with grpc.aio.secure_channel(f"{HOST}:{PORT}", credentials) as channel:
event_stores = EventStoresStub(channel)
try:
await event_stores.EnsureEventStore(EnsureEventStoreRequest(Name="python-probe"))
except grpc.aio.AioRpcError as error:
print("without token:", error.code().name)
result = await event_stores.EnsureEventStore(
EnsureEventStoreRequest(Name="python-probe"),
metadata=[("authorization", f"Bearer {token}")],
)
print("with token:", type(result).__name__)


asyncio.run(main())
```

The development kernel uses a self-signed certificate. A development option may explicitly relax certificate
verification for localhost. Production behavior must retain normal certificate and hostname validation.
Run it from the activated development environment. Against `cratis/chronicle:16.38.2-development` it prints:

```text
without token: UNAUTHENTICATED
with token: CommandResult
```

The probe creates an event store named `python-probe` in the development kernel. Stopping the container removes it.

## First executable milestone

Expand Down Expand Up @@ -130,3 +233,22 @@ uncertainty against the core contracts and kernel behavior.
- Local quality gates and CI pass.
- Documentation names unsupported behavior and development-only exceptions.
- No compatibility, parity, support, security, performance, or maturity claim was added without approval.

## Troubleshooting

| Symptom | Likely cause and fix |
| --- | --- |
| `pip install -e ".[dev]"` fails while downloading `cratis_chronicle_contracts-16.38.2-py3-none-any.whl` | The install cannot reach GitHub release assets. Allow `github.com` and `release-assets.githubusercontent.com`, where the download redirects, through your proxy or firewall |
| `pip` reports that hashes do not match | The downloaded wheel differs from the pinned SHA-256. Do not remove the hash; report it in an issue |
| `docker run` fails with `port is already allocated` | Another Chronicle kernel or process uses port 35000. Stop it, or publish a different host port (`-p 127.0.0.1:35100:35000`) and use that port in the connection string, the `curl` URL, and the probe's `PORT` |
| `ssl.SSLEOFError` or a refused connection right after `docker run` | The kernel is still starting. Wait until the token check succeeds, then retry |
| gRPC `UNAVAILABLE` with `CERTIFICATE_VERIFY_FAILED` in the details | The channel does not trust the kernel's self-signed certificate. Trust it explicitly for local development, as the probe does |
| gRPC `UNAUTHENTICATED` | The call carried no `authorization` metadata, or its token expired. Obtain a fresh token and attach it to every call |

## Next steps

- Pick up [connection-string parsing](https://github.com/Cratis/Chronicle.Python/issues/2), then
[async OAuth token handling](https://github.com/Cratis/Chronicle.Python/issues/3).
- Read Chronicle's [Building a Chronicle client](https://www.cratis.io/chronicle/building-a-client/) guide for
the cross-client contract.
- Follow [CONTRIBUTING.md](../CONTRIBUTING.md) for the required checks before opening a pull request.
17 changes: 15 additions & 2 deletions Documentation/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
# Getting started

Chronicle.Python does not yet expose a usable client API or published package. This page will become the
installation and first-append guide when the initial authenticated append milestone passes its tests.
installation and first-append guide when the
[initial authenticated append milestone](https://github.com/Cratis/Chronicle.Python/issues/4) passes its tests.

## What works today

| You want to… | Status |
| --- | --- |
| `pip install cratis-chronicle` from PyPI | Not possible. No package is published |
| Connect to Chronicle and append events from Python | Not possible through this package yet. It exposes only `__version__` |
| Build the client from source and run its checks | Supported. See [Development setup](../README.md#development-setup) |
| Use Chronicle from another language now | Use the [.NET](https://github.com/Cratis/Chronicle), [TypeScript](https://github.com/Cratis/Chronicle.TypeScript), [Kotlin/Java](https://github.com/Cratis/Chronicle.Kotlin), or [Elixir](https://github.com/Cratis/Chronicle.Elixir) client |

## Contribute

To contribute now, follow [Building the Chronicle Python client](client-development-guide.md) and the repository
[contribution guide](../CONTRIBUTING.md).
[contribution guide](../CONTRIBUTING.md). The development guide shows how to run a local kernel and check the
token and gRPC path before you write client code.
31 changes: 27 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,19 @@ contracts. Python joins the existing [.NET](https://github.com/Cratis/Chronicle)
and [Elixir](https://github.com/Cratis/Chronicle.Elixir) Chronicle clients.

> [!IMPORTANT]
> The client is in its initial implementation stage. No package has been published, and no compatibility,
> feature-parity, or support commitment is implied.
> The client is in its initial implementation stage. It has no usable client API yet: the `cratis_chronicle`
> package exposes only `__version__`. Nothing is published to PyPI, and no compatibility, feature-parity, or
> support commitment is implied.

## Current status

| Area | Status |
| --- | --- |
| Client API (connect, authenticate, append) | Not implemented. Tracked by [the first authenticated append milestone](https://github.com/Cratis/Chronicle.Python/issues/4) |
| `cratis-chronicle` on PyPI | Not published. Install from a source checkout |
| Generated contracts (`cratis-chronicle-contracts`) | Not on PyPI. Installed automatically from a SHA-256-pinned wheel attached to the [Chronicle v16.38.2 release](https://github.com/Cratis/Chronicle/releases/tag/v16.38.2) |
| Python versions | 3.10 or newer; CI runs 3.10, 3.11, 3.12, 3.13, and 3.14 |
| Shared Chronicle documentation (language tabs) | Not integrated. Tracked by [Python examples in shared Chronicle documentation](https://github.com/Cratis/Chronicle.Python/issues/5) |

## Start contributing

Expand All @@ -27,16 +38,28 @@ Chronicle kernel.

## Development setup

Python 3.10 or newer is required.
Python 3.10 or newer is required. The install downloads the contracts wheel from `github.com`, so it needs network
access to GitHub release assets as well as PyPI.

```shell
git clone https://github.com/Cratis/Chronicle.Python.git
cd Chronicle.Python
python -m venv .venv
source .venv/bin/activate # Windows PowerShell: .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

Run the same checks as CI:
Confirm that the package and its generated contracts import:

```shell
python -c "import cratis_chronicle, cratis_chronicle_contracts; print(cratis_chronicle.__version__)"
```

The command prints a development version derived from Git, such as `0.0.1.dev47+g…`. It prints `0.0.0` when the
package metadata cannot be found, which usually means the editable install did not run in the active environment.

Run the same checks as CI (the full list, with expected results, is in [CONTRIBUTING.md](CONTRIBUTING.md#required-checks)):

```shell
ruff format --check .
Expand Down
Loading