Skip to content

Latest commit

 

History

History
190 lines (130 loc) · 6.05 KB

File metadata and controls

190 lines (130 loc) · 6.05 KB

observer-python

Minimal Python-facing provider library for Observer.

If you are new to this surface, start with HOWTO.md first.

The most important framing is this: Python is usually the authoring surface, not the semantic subject under test.

In most successful Observer uses of Python, the Python tests are expressing verification against some other real subject:

  • a CLI
  • a service
  • a package artifact
  • a generated file set
  • a workflow step
  • a protocol boundary

That is the preferred pattern. Do not reduce Observer to "run one Python wrapper script and trust its exit code" if what you actually need is granular verification of a richer product surface.

This aims at the same DX bar as the Rust, TypeScript, and C libraries:

  • authors write describe(...), test(...), it(...), and expect(...)
  • default identity is derived deterministically from explicit suite path plus test title
  • optional id= provides a stable override when needed
  • observation is bounded and opt-in through ctx.observe().*
  • direct host and embedded observe dispatch are owned by the library

The common path stays human-first. The deterministic boundary stays explicit.

Preferred Usage Pattern

Preferred:

  • author many small Python tests
  • make each test name a real product behavior
  • expose those tests through the Observer provider host boundary
  • let Observer inventory, suites, reports, and product stages operate on those granular targets

Anti-pattern:

  • write one orchestration-heavy Python wrapper
  • expose only that wrapper as one test target
  • treat wrapper exit status as the proof

The preferred pattern gives you meaningful target identity, better failure localization, and much better derived artifacts.

Files

  • HOWTO.md: detailed user manual covering authoring, determinism, host transport, inventory derivation, and end-to-end workflow
  • observer.py: public API
  • tests/test_observer.py: stdlib-only self-test coverage for the Python SDK
  • example_smoke.py: tiny collection and execution example
  • host_example.py: tiny list/run host example
  • host_embed_example.py: own-main style observe namespace example
  • pyproject.toml: installable packaging metadata for the standalone Python module
  • starter/: runnable project-shaped example with provider host, inventory derivation, suite run, and snapshot verification
  • starter-embedded/: runnable app-shaped example where the application keeps main() and routes observe ... through its own CLI
  • starter-failure/: runnable failing companion showing the same provider flow with one intentionally failing exported test

If you want the full end-to-end workflow rather than isolated snippets, start with starter/, then read starter-embedded/, and then compare those with starter-failure/.

Minimal Shape

from observer import collect_tests, describe, expect, test


def build_tests():
    with describe("database"):
        @test("access to the database")
        def _(ctx):
            ctx.stdout("ok\n")
            expect(True).to_be_truthy()


tests = collect_tests(build_tests)

When id is omitted, Observer derives a deterministic identity from suite path, test title, and duplicate occurrence order.

If a test wants a refactor-stable identity, it opts into id explicitly:

@test("access to the database", id="database/access")
def _(ctx):
    expect(True).to_be_truthy()

If a test wants to emit observational data, it uses the author context directly:

@test("access to the database", id="database/access")
def _(ctx):
    observer = ctx.observe()
    assert observer.metric("wall_time_ns", 104233.0)
    assert observer.vector("request_latency_ns", [1000.0, 1100.0, 980.0])
    assert observer.tag("resource_path", "fixtures/config.json")
    expect(True).to_be_truthy()

Validation Rules

  • explicit id, when present, must be non-empty
  • resolved canonical identities must be unique
  • resolved targets must be unique
  • deterministic sorting is by canonical name, then target

In this first cut, the resolved identity is used for both canonical name and target.

Smoke Example

python3 lib/python/example_smoke.py

Self-Test Suite

python3 -m unittest discover -s lib/python/tests -p 'test_*.py'

Packaging

The Python library now includes minimal packaging metadata in pyproject.toml.

That means the module can be installed directly from this folder:

python3 -m pip install ./lib/python

Host Example

python3 lib/python/host_example.py list
python3 lib/python/host_example.py observe --target pkg::smoke --timeout-ms 1000
python3 lib/python/host_example.py run --target pkg::fail --timeout-ms 1000

The library owns the standard provider host transport for Python too. A direct host can stay nearly trivial:

from observer import collect_tests, describe, expect, observer_host_main, test


def build_tests():
    with describe("pkg"):
        @test("smoke test", id="pkg::smoke")
        def smoke(ctx):
            ctx.stdout("ok\n")
            expect(True).to_be_truthy()


if __name__ == "__main__":
    observer_host_main("python", collect_tests(build_tests))

For developer-facing usage, prefer observe. run remains available for compatibility with the standardized outer provider contract.

Own Main Integration

If a project already owns its CLI, the library can also serve an embedded observe namespace:

import sys

from observer import collect_tests, describe, expect, observer_host_dispatch_embedded, test


def build_tests():
    with describe("pkg"):
        @test("embedded smoke test", id="pkg::embedded-smoke")
        def smoke(ctx):
            ctx.stdout("ok\n")
            expect(True).to_be_truthy()


def app_main(argv):
    print("app main", " ".join(argv[1:]))


if __name__ == "__main__":
    tests = collect_tests(build_tests)
    if len(sys.argv) > 1 and sys.argv[1] == "observe":
        observer_host_dispatch_embedded("python", "observe", tests)
    else:
        app_main(sys.argv)

If you want the full app-shaped workflow rather than just the embedded snippet, read starter-embedded/.