diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..27f114c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Report incorrect Chronicle.Python behavior +title: "" +labels: "kind/bug" +assignees: "" +--- + +## Description + +Describe the incorrect behavior. + +## Steps to reproduce + +1. ... +2. ... +3. ... + +## Expected behavior + +Describe what should happen. + +## Actual behavior + +Describe what happens instead. + +## Environment + +- OS: +- Python version: +- Chronicle.Python revision or version: +- Chronicle kernel version: +- Storage provider and topology, if relevant: + +## Additional context + +Remove credentials, access tokens, customer data, and private addresses before submitting. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8312ffe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security report + url: https://github.com/Cratis/Chronicle.Python/security/advisories/new + about: Report suspected vulnerabilities privately diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a28f9a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,31 @@ +--- +name: Feature request +about: Propose a bounded Python client capability +title: "" +labels: "kind/feature" +assignees: "" +--- + +## User need + +What Python client job should become possible? + +## Desired behavior + +Describe the smallest observable behavior, including failure behavior. + +## Chronicle contract + +Link the relevant public Chronicle contract or documentation when known. + +## Non-goals + +What should this proposal deliberately not implement? + +## Acceptance evidence + +How should the behavior be tested against the named Python, Chronicle, provider, and topology profile? + +## Additional context + +Do not include customer identity, credentials, access tokens, private conversations, or restricted details. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..064ee96 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,40 @@ +# Summary + +Optional short context. Remove this section when the bullets are sufficient. + +## Added + +- User-facing addition + +## Changed + +- User-facing change + +## Fixed + +- User-facing fix + +## Removed + +- User-facing removal + +## Security + +- Security-relevant change without restricted details + +## Deprecated + +- User-facing deprecation + +## Verification + +- [ ] `ruff format --check .` +- [ ] `ruff check .` +- [ ] `mypy src` +- [ ] `pytest` +- [ ] `python -m build` +- [ ] `python -m twine check dist/*` +- [ ] Unsupported behavior and development-only exceptions remain explicit + +Remove empty sections before opening the pull request. Do not include prompts, transcripts, credentials, customer +data, or private context. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..34b18b5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: Build + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + branches: + - "**" + push: + branches: + - main + +permissions: + contents: read + +jobs: + quality: + name: Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v7 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install development dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Check formatting + run: ruff format --check . + + - name: Lint + run: ruff check . + + - name: Type check + run: mypy src + + - name: Test + run: pytest + + - name: Build distributions + run: python -m build + + - name: Check distributions + run: python -m twine check dist/* diff --git a/.github/workflows/markdown-verification.yml b/.github/workflows/markdown-verification.yml new file mode 100644 index 0000000..77f4543 --- /dev/null +++ b/.github/workflows/markdown-verification.yml @@ -0,0 +1,28 @@ +name: Markdown Verification + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + workflow_dispatch: + pull_request: + branches: + - "**" + paths: + - "**/*.md" + +permissions: + contents: read + +jobs: + markdown-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Lint Markdown + uses: DavidAnson/markdownlint-cli2-action@v18 + with: + globs: "**/*.md" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..9298a35 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,54 @@ +name: Publish + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Build distributions + run: | + python -m pip install --upgrade pip + python -m pip install build twine + python -m build + python -m twine check dist/* + + - name: Upload distributions + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + if-no-files-found: error + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/verify-semver-label.yml b/.github/workflows/verify-semver-label.yml new file mode 100644 index 0000000..e8274ad --- /dev/null +++ b/.github/workflows/verify-semver-label.yml @@ -0,0 +1,29 @@ +name: Verify Semver Label + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +on: + pull_request: + types: [opened, reopened, synchronize, labeled, unlabeled] + branches: + - main + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: Require exactly one release disposition label + env: + LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }} + run: | + count=$(printf '%s' "$LABELS" | jq '[.[] | select(. == "major" or . == "minor" or . == "patch" or . == "no-release")] | length') + if [ "$count" -eq 1 ]; then + exit 0 + fi + echo "::error::Add exactly one of major, minor, patch, or no-release." + exit 1 diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..67d2ae5 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "MD013": false +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9c693d2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Chronicle.Python repository instructions + +This is a Cratis framework repository containing an experimental Python client for Chronicle. + +## Boundaries + +- The idiomatic Python API lives under `src/cratis_chronicle`. +- Generated protobuf contracts are owned by the core `Cratis/Chronicle` repository and consumed as a package. +- Do not copy generated files into this repository or manually mirror protobuf messages. +- Do not infer feature parity, compatibility, maturity, or support from another Chronicle client. +- Public API design requires maintainer review before broad implementation. + +## Engineering rules + +- Use Python 3.10-compatible syntax. +- Network APIs are async-first. +- Use American English. +- Add type annotations and keep `mypy` strict mode passing. +- Keep `ruff format`, `ruff check`, `pytest`, package build, and `twine check` passing. +- Add tests for behavior and failure paths; a stub that silently succeeds is a bug. +- Never commit credentials, tokens, customer data, private conversations, transcripts, or generated local artifacts. +- Prefer narrow, reviewable changes and explicit unsupported outcomes. + +## Before completion + +Run: + +```shell +ruff format --check . +ruff check . +mypy src +pytest +rm -rf dist +python -m build +python -m twine check dist/* +``` diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4f8aad4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,15 @@ +# Community conduct + +Chronicle.Python welcomes constructive contributions from people with different backgrounds and levels of +experience. + +Participants are expected to: + +- communicate respectfully and focus criticism on ideas and code; +- make space for questions and good-faith mistakes; +- avoid harassment, discrimination, threats, personal attacks, and unwanted disclosure of private information; +- keep public collaboration free of credentials, customer data, private conversations, and security details; and +- follow maintainer direction when a discussion needs to pause, narrow, or move to a safer channel. + +Unacceptable behavior may be edited, removed, or result in temporary or permanent loss of participation access. +Report conduct concerns privately to `post@cratis.io`. Do not use a public issue for sensitive reports. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9e1e9a4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,59 @@ +# Contributing to Chronicle.Python + +Thank you for helping build the Python client for Chronicle. + +## Before starting + +- Read the [client development guide](Documentation/client-development-guide.md). +- Discuss public API choices in an issue before implementing a broad surface. +- Keep changes bounded to one observable client milestone. +- Do not copy generated protobuf files or hand-maintain wire contracts in this repository. +- Do not claim compatibility, parity, maturity, security, performance, or support without corresponding evidence + and maintainer approval. + +## Local setup + +```shell +python -m venv .venv +source .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install -e ".[dev]" +``` + +On Windows PowerShell, activate with `.venv\Scripts\Activate.ps1`. + +## Required checks + +```shell +ruff format --check . +ruff check . +mypy src +pytest +rm -rf dist +python -m build +python -m twine check dist/* +``` + +All checks must pass before review. New behavior requires tests, including failure behavior where applicable. + +## API principles + +- Prefer an async-first API for network operations. +- Use idiomatic Python names and types rather than exposing generated protobuf naming as the primary API. +- Provide safe defaults while keeping connection, authentication, serialization, and lifecycle behavior explicit. +- Keep generated contracts behind an internal boundary unless a low-level escape hatch is deliberately accepted. +- Make unsupported behavior visible; never silently succeed with a stub. +- Preserve Chronicle's wire-value contract described in the core repository. + +## Pull requests + +- Use a focused branch and a concise imperative commit message. +- Complete the pull request template and remove empty sections. +- Add exactly one of `major`, `minor`, or `patch` when the change is release-bearing; use `no-release` for changes + that intentionally produce no package release. +- Never include credentials, access tokens, customer data, private conversations, or agent transcripts. +- Maintainers may request changes to API shape before accepting an implementation, even when the code works. + +## Reporting security concerns + +Do not open a public issue for a suspected vulnerability. Follow [SECURITY.md](SECURITY.md). diff --git a/Documentation/client-development-guide.md b/Documentation/client-development-guide.md new file mode 100644 index 0000000..a81d221 --- /dev/null +++ b/Documentation/client-development-guide.md @@ -0,0 +1,123 @@ +# Building the Chronicle Python client + +This guide defines the starting sequence for a Python client. It keeps the first contribution small enough to +review and verify while preserving Chronicle's generated wire contract. + +## Architecture boundary + +Chronicle exposes gRPC services generated from code-first contracts in the core +[`Cratis/Chronicle`](https://github.com/Cratis/Chronicle) repository. + +The core release pipeline owns a low-level Python package generated from those `.proto` files. This repository +owns the idiomatic client that consumes that package. Generated messages and stubs must not be copied, edited, or +recreated by hand here. + +```text +Chronicle C# contracts + -> generated .proto files + -> generated Python contracts package + -> idiomatic Chronicle.Python API +``` + +## 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. + +Start the development image: + +```shell +docker run --rm -p 35000:35000 cratis/chronicle:latest-development +``` + +Use the explicit local-development connection string: + +```text +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. + +## Authentication contract + +Request a token from: + +```text +https://localhost:35000/connect/token +``` + +Send an `application/x-www-form-urlencoded` body with: + +```text +grant_type=client_credentials +client_id= +client_secret= +``` + +Parse `access_token` from the JSON response. Attach the current token to each new gRPC call as metadata: + +```text +authorization: Bearer +``` + +Token acquisition, caching, expiry, refresh, and call interception should remain separate from the channel. Do +not permanently bake one expiring token into channel headers. + +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. + +## First executable milestone + +Implement and verify this order before expanding the API: + +1. Parse explicit connection options. +2. Establish the TLS gRPC channel. +3. Obtain and propagate a bearer token. +4. Ensure one event store. +5. Ensure its namespace; use `Default` for the first exercise. +6. Register one event type and a non-empty JSON schema. +7. Append one event to `event-log`. +8. Assert that the response reports success and a sequence number. +9. Close the channel and token resources deterministically. + +Stop there. Projections, reducers, reactors, subscriptions, automatic artifact discovery, reconnect behavior, and +framework integrations each add ordering or lifecycle behavior and should have separate evidence. + +## Suggested module boundaries + +The final names are subject to API review, but contributions should keep these responsibilities separate: + +- public connection options and validation; +- OAuth token acquisition and refresh; +- gRPC channel creation and metadata interception; +- generated-stub access behind an internal service registry; +- wire-value conversion; +- event-store and namespace setup; +- event-type/schema registration; and +- event-log append/read operations. + +Do not create one large client class that owns every responsibility. + +## Wire-value conformance + +The core Chronicle repository owns the value contract. Initial conversion tests must cover at least: + +- UUIDs as canonical strings; +- dates and times in the documented Chronicle forms; +- Chronicle's duration form; +- concept wrappers serialized as their primitive value; +- exact schema and payload property naming; and +- a non-empty schema for a registered event type. + +Cross-client implementation similarity is useful evidence, but another client is not protocol authority. Resolve +uncertainty against the core contracts and kernel behavior. + +## Definition of done for a milestone + +- The public API was discussed or accepted in its issue. +- Unit tests cover success and relevant failure behavior. +- An integration test exercises the exact kernel profile when network behavior changes. +- 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. diff --git a/Documentation/getting-started.md b/Documentation/getting-started.md new file mode 100644 index 0000000..92efa8f --- /dev/null +++ b/Documentation/getting-started.md @@ -0,0 +1,7 @@ +# 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. + +To contribute now, follow [Building the Chronicle Python client](client-development-guide.md) and the repository +[contribution guide](../CONTRIBUTING.md). diff --git a/README.md b/README.md index 19a06f5..71d150d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,74 @@ -# Chronicle.Python -Experimental Python client for Cratis Chronicle +# Chronicle for Python + +[![Build](https://github.com/Cratis/Chronicle.Python/actions/workflows/build.yml/badge.svg)](https://github.com/Cratis/Chronicle.Python/actions/workflows/build.yml) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +This repository is the experimental Python client for [Cratis Chronicle](https://github.com/Cratis/Chronicle). +It is being established as an idiomatic, async-first layer over Chronicle's generated gRPC contracts. + +> [!IMPORTANT] +> The client is in its initial implementation stage. No package has been published, and no compatibility, +> feature-parity, or support commitment is implied. + +## Start contributing + +The repository currently provides the package structure, quality gates, contribution workflow, and an ordered +client-development guide. The first implementation milestone is a minimal authenticated append against a local +Chronicle kernel. + +1. Read [CONTRIBUTING.md](CONTRIBUTING.md). +2. Follow the [client development guide](Documentation/client-development-guide.md). +3. Pick an open issue with a bounded acceptance checklist. +4. Submit a pull request from a fork or branch. + +## Development setup + +Python 3.10 or newer is required. + +```shell +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: + +```shell +ruff format --check . +ruff check . +mypy src +pytest +python -m build +python -m twine check dist/* +``` + +## Repository map + +| Path | Purpose | +| --- | --- | +| `src/cratis_chronicle/` | Idiomatic Python client package | +| `tests/` | Unit and contract-facing tests | +| `Documentation/` | Python-specific setup and contributor guidance | +| `Samples/` | Runnable examples added as client milestones become available | + +The non-idiomatic generated protobuf surface is owned and released from the core Chronicle repository. It should +not be copied or manually mirrored here. + +## Scope + +The first bounded path covers: + +1. a TLS gRPC channel to a local kernel; +2. OAuth client-credentials authentication; +3. bearer-token propagation on calls; +4. event-store and namespace setup; +5. registration of one event type and schema; and +6. appending one event with an asserted success response. + +Projection, reducer, reactor, subscription, automatic-discovery, and reconnect behavior follows only after this +path has executable evidence. + +## License + +Chronicle.Python is licensed under the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..afab575 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,16 @@ +# Security policy + +## Reporting a vulnerability + +Please report suspected vulnerabilities privately through GitHub's **Report a vulnerability** flow in the +Security tab of this repository. + +Do not include secrets, access tokens, customer data, production addresses, or exploit details in a public issue, +discussion, or pull request. + +A maintainer will acknowledge the report and coordinate the next safe step. This policy does not establish a +response-time, remediation-time, or support commitment. + +## Supported versions + +No Chronicle.Python version is currently published or designated as supported. diff --git a/Samples/README.md b/Samples/README.md new file mode 100644 index 0000000..4e2f057 --- /dev/null +++ b/Samples/README.md @@ -0,0 +1,7 @@ +# Samples + +Runnable samples will be added with the client milestones they exercise. A sample must use the released or local +client package, contain no credentials beyond documented local-development defaults, and run in CI before it is +presented as working. + +The first planned sample will authenticate to a local Chronicle development kernel and append one event. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efb5d0e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,69 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "cratis-chronicle" +dynamic = ["version"] +description = "Experimental Python client for Cratis Chronicle" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Cratis", email = "post@cratis.io" }] +keywords = ["chronicle", "event-sourcing", "grpc"] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [] + +[project.optional-dependencies] +dev = [ + "build", + "mypy", + "pytest", + "pytest-cov", + "ruff", + "twine", +] + +[project.urls] +Homepage = "https://github.com/Cratis/Chronicle.Python" +Documentation = "https://github.com/Cratis/Chronicle.Python/tree/main/Documentation" +Repository = "https://github.com/Cratis/Chronicle.Python.git" +Issues = "https://github.com/Cratis/Chronicle.Python/issues" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.version.raw-options] +fallback_version = "0.0.0" + +[tool.hatch.build.targets.wheel] +packages = ["src/cratis_chronicle"] + +[tool.pytest.ini_options] +addopts = "--strict-config --strict-markers --cov=cratis_chronicle --cov-report=term-missing" +testpaths = ["tests"] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["B", "E", "F", "I", "UP"] + +[tool.mypy] +python_version = "3.10" +strict = true +packages = ["cratis_chronicle"] diff --git a/src/cratis_chronicle/__init__.py b/src/cratis_chronicle/__init__.py new file mode 100644 index 0000000..94647c5 --- /dev/null +++ b/src/cratis_chronicle/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +"""Experimental Python client for Cratis Chronicle.""" + +from importlib.metadata import PackageNotFoundError, version + +try: + __version__ = version("cratis-chronicle") +except PackageNotFoundError: + __version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/src/cratis_chronicle/py.typed b/src/cratis_chronicle/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..b5b6490 --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,8 @@ +# Copyright (c) Cratis. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import cratis_chronicle + + +def test_package_exposes_a_version() -> None: + assert cratis_chronicle.__version__