feat(registry): add PackageType, PackageId, PackageReference, PackageContent core types #31
@@ -0,0 +1,203 @@
|
||||
Feature: Package Registry Core Data Types
|
||||
As a developer
|
||||
I want PackageId, PackageReference, PackageType, and PackageContent types
|
||||
So that I can work with the Package Registry Standard v1.0.0
|
||||
|
||||
Background:
|
||||
Given I have imported the registry core types
|
||||
|
||||
# ── PackageType ──────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: PackageType has all required members
|
||||
Then PackageType should have member ACTOR with value "act"
|
||||
And PackageType should have member GRAPH with value "grh"
|
||||
And PackageType should have member STREAM with value "str"
|
||||
And PackageType should have member AGENT with value "agt"
|
||||
And PackageType should have member TEMPLATE with value "tpl"
|
||||
And PackageType should have member SKILL with value "skl"
|
||||
And PackageType should have member MCP with value "mcp"
|
||||
And PackageType should have member LSP with value "lsp"
|
||||
|
||||
# ── PackageId ────────────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Valid PackageId from_string succeeds
|
||||
Given a valid actor PackageId string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
When I parse it with PackageId.from_string
|
||||
Then the PackageId should have type ACTOR
|
||||
And the PackageId should have sha1_hex "0123456789abcdef0123456789abcdef01234567"
|
||||
And the PackageId should have id_string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
|
||||
Scenario: Valid PackageId from_string for each type
|
||||
Given a valid GRAPH PackageId string "pkg_grh_deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
When I parse it with PackageId.from_string
|
||||
Then the PackageId should have type GRAPH
|
||||
And the PackageId should have sha1_hex "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
|
||||
Scenario: PackageId empty string raises InvalidPackageIdError
|
||||
Given an empty PackageId string
|
||||
When I parse the empty string with PackageId.from_string
|
||||
Then an InvalidPackageIdError should be raised with message containing "must not be empty"
|
||||
|
||||
Scenario: PackageId missing pkg_ prefix raises InvalidPackageIdError
|
||||
Given a PackageId string "act_0123456789abcdef0123456789abcdef01234567" missing the pkg_ prefix
|
||||
When I parse it with PackageId.from_string
|
||||
Then an InvalidPackageIdError should be raised with message containing "must start with 'pkg_'"
|
||||
|
||||
Scenario: PackageId unknown type prefix raises InvalidPackageIdError
|
||||
Given a PackageId string "pkg_xyz_0123456789abcdef0123456789abcdef01234567" with unknown type prefix
|
||||
When I parse it with PackageId.from_string
|
||||
Then an InvalidPackageIdError should be raised with message containing "Unknown package type prefix"
|
||||
|
||||
Scenario: PackageId short SHA1 raises InvalidPackageIdError
|
||||
Given a PackageId string "pkg_act_short" with invalid SHA1
|
||||
When I parse it with PackageId.from_string
|
||||
Then an InvalidPackageIdError should be raised with message containing "40 hex characters"
|
||||
|
||||
Scenario: PackageId uppercase SHA1 raises InvalidPackageIdError
|
||||
Given a PackageId string "pkg_act_0123456789ABCDEF0123456789ABCDEF01234567" with uppercase hex
|
||||
When I parse it with PackageId.from_string
|
||||
Then an InvalidPackageIdError should be raised with message containing "lowercase hex characters"
|
||||
|
||||
Scenario: PackageId is hashable
|
||||
Given a valid actor PackageId string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
When I parse it with PackageId.from_string
|
||||
Then the PackageId should be hashable
|
||||
|
||||
Scenario: PackageId direct construction
|
||||
Given I construct a PackageId directly with type GRAPH and sha1 "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
Then the PackageId should have type GRAPH
|
||||
And the PackageId should have id_string "pkg_grh_deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
||||
|
||||
Scenario: PackageId direct construction with invalid SHA1 raises InvalidPackageIdError
|
||||
Given I construct a PackageId directly with type ACTOR and sha1 "bad"
|
||||
Then an InvalidPackageIdError should be raised with message containing "40 hex characters"
|
||||
|
||||
# ── PackageReference ─────────────────────────────────────────────────────
|
||||
|
||||
Scenario: Registry reference parsing
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search@latest"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the reference_type should be REGISTRY
|
||||
And the server should be "registry.cleverthis.com"
|
||||
And the namespace should be "acme"
|
||||
And the name should be "web_search"
|
||||
And the version should be "latest"
|
||||
|
||||
Scenario: Registry reference without version defaults to latest
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the version should be "latest"
|
||||
|
||||
Scenario: ID reference parsing
|
||||
Given a package reference string "ID:pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the reference_type should be ID
|
||||
And the id_string should be "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
|
||||
Scenario: LOCAL reference parsing
|
||||
Given a package reference string "local:path/to/component"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the reference_type should be LOCAL
|
||||
And the name should be "path/to/component"
|
||||
|
||||
Scenario: PackageReference round-trips original_reference
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search@latest"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the original_reference should be "registry.cleverthis.com:acme/web_search@latest"
|
||||
|
||||
Scenario: Empty reference string raises ValueError
|
||||
Given an empty package reference string
|
||||
When I parse the empty string with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must not be empty"
|
||||
|
||||
Scenario: Invalid registry reference without slash raises ValueError
|
||||
Given a package reference string "server:missing"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "namespace/name"
|
||||
|
||||
Scenario: PackageReference is hashable
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search@latest"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the PackageReference should be hashable
|
||||
|
||||
Scenario: PackageId str returns id_string
|
||||
Given a valid actor PackageId string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
When I parse it with PackageId.from_string
|
||||
Then str of the PackageId should be "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
|
||||
Scenario: PackageId repr returns debug representation
|
||||
Given a valid actor PackageId string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
When I parse it with PackageId.from_string
|
||||
Then repr of the PackageId should be "PackageId('pkg_act_0123456789abcdef0123456789abcdef01234567')"
|
||||
|
||||
Scenario: PackageReference str returns original_reference
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search@latest"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then str of the PackageReference should be "registry.cleverthis.com:acme/web_search@latest"
|
||||
|
||||
Scenario: PackageReference repr returns debug representation
|
||||
Given a package reference string "registry.cleverthis.com:acme/web_search@latest"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then repr of the PackageReference should be "PackageReference('registry.cleverthis.com:acme/web_search@latest')"
|
||||
|
||||
Scenario: ID reference with empty ID raises ValueError
|
||||
Given a package reference string "ID:"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "ID reference must contain a Package ID"
|
||||
|
||||
Scenario: ID reference with invalid PackageId raises ValueError
|
||||
Given a package reference string "ID:not_a_valid_id"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "Invalid ID reference"
|
||||
|
||||
Scenario: LOCAL reference with empty path raises ValueError
|
||||
Given a package reference string "local:"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must have a path after"
|
||||
|
||||
Scenario: Registry reference without colon separator raises ValueError
|
||||
Given a package reference string "nocolonserver"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must be in format"
|
||||
|
||||
Scenario: Registry reference with empty server raises ValueError
|
||||
Given a package reference string ":ns/name"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must include a server name"
|
||||
|
||||
Scenario: Registry reference with empty version after at raises ValueError
|
||||
Given a package reference string "srv:ns/name@"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then the version should be "latest"
|
||||
|
||||
Scenario: Registry reference with empty namespace raises ValueError
|
||||
Given a package reference string "srv:/name"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must include a namespace"
|
||||
|
||||
Scenario: Registry reference with empty name raises ValueError
|
||||
Given a package reference string "srv:ns/"
|
||||
When I parse it with PackageReference.from_string
|
||||
Then a ValueError should be raised with message containing "must include a name"
|
||||
|
||||
# ── PackageContent ───────────────────────────────────────────────────────
|
||||
|
||||
Scenario: PackageContent wraps fetched data
|
||||
Given a PackageId from string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
And a content dictionary with key "name" and value "my-actor"
|
||||
When I create a PackageContent with the id and content
|
||||
Then the PackageContent id should be "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
And the PackageContent content should have key "name" with value "my-actor"
|
||||
And the PackageContent fetched_at should be set
|
||||
|
||||
Scenario: PackageContent with original_reference
|
||||
Given a PackageId from string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
And a content dictionary with key "name" and value "my-actor"
|
||||
When I create a PackageContent with id, content, and original_reference "registry.cleverthis.com:acme/actor@latest"
|
||||
Then the PackageContent original_reference should be "registry.cleverthis.com:acme/actor@latest"
|
||||
|
||||
Scenario: PackageContent is hashable
|
||||
Given a PackageId from string "pkg_act_0123456789abcdef0123456789abcdef01234567"
|
||||
And a content dictionary with key "name" and value "my-actor"
|
||||
When I create a PackageContent with the id and content
|
||||
Then the PackageContent should be hashable
|
||||
@@ -0,0 +1,336 @@
|
||||
"""
|
||||
Step definitions for Package Registry Core Data Types BDD tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
|
||||
from cleveractors.registry.exceptions import InvalidPackageIdError
|
||||
from cleveractors.registry.types import (
|
||||
PackageContent,
|
||||
PackageId,
|
||||
PackageReference,
|
||||
PackageType,
|
||||
ReferenceType,
|
||||
)
|
||||
|
||||
|
||||
@given("I have imported the registry core types")
|
||||
def step_import_registry_types(context: Context) -> None:
|
||||
context.PackageType = PackageType
|
||||
context.PackageId = PackageId
|
||||
context.PackageReference = PackageReference
|
||||
context.PackageContent = PackageContent
|
||||
context.ReferenceType = ReferenceType
|
||||
|
||||
|
||||
# ── PackageType steps ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@then('PackageType should have member {member} with value "{value}"')
|
||||
def step_package_type_member(context: Context, member: str, value: str) -> None:
|
||||
enum_member = PackageType[member]
|
||||
assert enum_member.value == value, (
|
||||
f"Expected {member}.value={value!r}, got {enum_member.value!r}"
|
||||
)
|
||||
|
||||
|
||||
# ── PackageId steps ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given('a valid {pkg_type} PackageId string "{raw_id}"')
|
||||
def step_given_valid_package_id(context: Context, pkg_type: str, raw_id: str) -> None:
|
||||
context.raw_id = raw_id
|
||||
context.expected_type = PackageType[pkg_type.upper()]
|
||||
|
||||
|
||||
@given("an empty PackageId string")
|
||||
def step_given_empty_package_id(context: Context) -> None:
|
||||
context.raw_id = ""
|
||||
|
||||
|
||||
@given('a PackageId string "{raw_id}" missing the pkg_ prefix')
|
||||
def step_given_missing_prefix(context: Context, raw_id: str) -> None:
|
||||
context.raw_id = raw_id
|
||||
|
||||
|
||||
@given('a PackageId string "{raw_id}" with unknown type prefix')
|
||||
def step_given_unknown_prefix(context: Context, raw_id: str) -> None:
|
||||
context.raw_id = raw_id
|
||||
|
||||
|
||||
@given('a PackageId string "{raw_id}" with invalid SHA1')
|
||||
def step_given_bad_sha1(context: Context, raw_id: str) -> None:
|
||||
context.raw_id = raw_id
|
||||
|
||||
|
||||
@given('a PackageId string "{raw_id}" with uppercase hex')
|
||||
def step_given_uppercase_sha1(context: Context, raw_id: str) -> None:
|
||||
context.raw_id = raw_id
|
||||
|
||||
|
||||
@when("I parse it with PackageId.from_string")
|
||||
def step_when_parse_package_id(context: Context) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.package_id = PackageId.from_string(context.raw_id)
|
||||
except InvalidPackageIdError as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@when("I parse the empty string with PackageId.from_string")
|
||||
def step_when_parse_empty_package_id(context: Context) -> None:
|
||||
context.raw_id = ""
|
||||
step_when_parse_package_id(context)
|
||||
|
||||
|
||||
@then("the PackageId should have type {expected_type}")
|
||||
def step_then_package_id_type(context: Context, expected_type: str) -> None:
|
||||
expected = PackageType[expected_type]
|
||||
assert context.package_id.package_type == expected, (
|
||||
f"Expected type {expected}, got {context.package_id.package_type}"
|
||||
)
|
||||
|
||||
|
||||
@then('the PackageId should have sha1_hex "{expected}"')
|
||||
def step_then_package_id_sha1(context: Context, expected: str) -> None:
|
||||
assert context.package_id.sha1_hex == expected, (
|
||||
f"Expected sha1_hex {expected!r}, got {context.package_id.sha1_hex!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the PackageId should have id_string "{expected}"')
|
||||
def step_then_package_id_string(context: Context, expected: str) -> None:
|
||||
assert context.package_id.id_string == expected, (
|
||||
f"Expected id_string {expected!r}, got {context.package_id.id_string!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('an InvalidPackageIdError should be raised with message containing "{phrase}"')
|
||||
def step_then_invalid_package_id_error_message(context: Context, phrase: str) -> None:
|
||||
assert context.error is not None, (
|
||||
"Expected InvalidPackageIdError but no error was raised"
|
||||
)
|
||||
assert isinstance(context.error, InvalidPackageIdError), (
|
||||
f"Expected InvalidPackageIdError, got {type(context.error).__name__}"
|
||||
)
|
||||
assert phrase in str(context.error), (
|
||||
f"Expected message containing {phrase!r}, got {context.error!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('a ValueError should be raised with message containing "{phrase}"')
|
||||
def step_then_value_error_message(context: Context, phrase: str) -> None:
|
||||
assert context.error is not None, "Expected ValueError but no error was raised"
|
||||
assert isinstance(context.error, ValueError), (
|
||||
f"Expected ValueError, got {type(context.error).__name__}"
|
||||
)
|
||||
assert phrase in str(context.error), (
|
||||
f"Expected message containing {phrase!r}, got {context.error!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the PackageId should be hashable")
|
||||
def step_then_package_id_hashable(context: Context) -> None:
|
||||
_ = hash(context.package_id)
|
||||
used_as_key = {context.package_id: "value"}
|
||||
assert used_as_key[context.package_id] == "value"
|
||||
|
||||
|
||||
@then('str of the PackageId should be "{expected}"')
|
||||
def step_then_package_id_str(context: Context, expected: str) -> None:
|
||||
assert str(context.package_id) == expected, (
|
||||
f"Expected str {expected!r}, got {str(context.package_id)!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('repr of the PackageId should be "{expected}"')
|
||||
def step_then_package_id_repr(context: Context, expected: str) -> None:
|
||||
assert repr(context.package_id) == expected, (
|
||||
f"Expected repr {expected!r}, got {repr(context.package_id)!r}"
|
||||
)
|
||||
|
||||
|
||||
@given('I construct a PackageId directly with type {pkg_type} and sha1 "{sha1}"')
|
||||
def step_given_direct_package_id(context: Context, pkg_type: str, sha1: str) -> None:
|
||||
context.expected_type = PackageType[pkg_type]
|
||||
context.error = None
|
||||
try:
|
||||
context.package_id = PackageId(
|
||||
package_type=PackageType[pkg_type], sha1_hex=sha1
|
||||
)
|
||||
except InvalidPackageIdError as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
# ── PackageReference steps ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given('a package reference string "{raw_ref}"')
|
||||
def step_given_reference_string(context: Context, raw_ref: str) -> None:
|
||||
context.raw_ref = raw_ref
|
||||
|
||||
|
||||
@when("I parse it with PackageReference.from_string")
|
||||
def step_when_parse_reference(context: Context) -> None:
|
||||
context.error = None
|
||||
try:
|
||||
context.package_ref = PackageReference.from_string(context.raw_ref)
|
||||
except ValueError as exc:
|
||||
context.error = exc
|
||||
|
||||
|
||||
@when("I parse the empty string with PackageReference.from_string")
|
||||
def step_when_parse_empty_reference(context: Context) -> None:
|
||||
context.raw_ref = ""
|
||||
step_when_parse_reference(context)
|
||||
|
||||
|
||||
@then("the reference_type should be {ref_type}")
|
||||
def step_then_ref_type(context: Context, ref_type: str) -> None:
|
||||
expected = ReferenceType[ref_type]
|
||||
assert context.package_ref.reference_type == expected, (
|
||||
f"Expected {expected}, got {context.package_ref.reference_type}"
|
||||
)
|
||||
|
||||
|
||||
@then('the server should be "{expected}"')
|
||||
def step_then_server(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.server == expected, (
|
||||
f"Expected server {expected!r}, got {context.package_ref.server!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the namespace should be "{expected}"')
|
||||
def step_then_namespace(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.namespace == expected, (
|
||||
f"Expected namespace {expected!r}, got {context.package_ref.namespace!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the name should be "{expected}"')
|
||||
def step_then_name(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.name == expected, (
|
||||
f"Expected name {expected!r}, got {context.package_ref.name!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the version should be "{expected}"')
|
||||
def step_then_version(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.version == expected, (
|
||||
f"Expected version {expected!r}, got {context.package_ref.version!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the id_string should be "{expected}"')
|
||||
def step_then_id_string(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.id_string == expected, (
|
||||
f"Expected id_string {expected!r}, got {context.package_ref.id_string!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the original_reference should be "{expected}"')
|
||||
def step_then_original_reference(context: Context, expected: str) -> None:
|
||||
assert context.package_ref.original_reference == expected, (
|
||||
f"Expected original_reference {expected!r}, "
|
||||
f"got {context.package_ref.original_reference!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the PackageReference should be hashable")
|
||||
def step_then_reference_hashable(context: Context) -> None:
|
||||
_ = hash(context.package_ref)
|
||||
used_as_key = {context.package_ref: "value"}
|
||||
assert used_as_key[context.package_ref] == "value"
|
||||
|
||||
|
||||
@then('str of the PackageReference should be "{expected}"')
|
||||
def step_then_reference_str(context: Context, expected: str) -> None:
|
||||
assert str(context.package_ref) == expected, (
|
||||
f"Expected str {expected!r}, got {str(context.package_ref)!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('repr of the PackageReference should be "{expected}"')
|
||||
def step_then_reference_repr(context: Context, expected: str) -> None:
|
||||
assert repr(context.package_ref) == expected, (
|
||||
f"Expected repr {expected!r}, got {repr(context.package_ref)!r}"
|
||||
)
|
||||
|
||||
|
||||
@given("an empty package reference string")
|
||||
def step_given_empty_reference(context: Context) -> None:
|
||||
context.raw_ref = ""
|
||||
|
||||
|
||||
# ── PackageContent steps ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@given('a PackageId from string "{raw_id}"')
|
||||
def step_given_package_id_from_string(context: Context, raw_id: str) -> None:
|
||||
context.package_id = PackageId.from_string(raw_id)
|
||||
|
||||
|
||||
@given('a content dictionary with key "{key}" and value "{value}"')
|
||||
def step_given_content_dict(context: Context, key: str, value: str) -> None:
|
||||
context.content_dict = {key: value}
|
||||
|
||||
|
||||
@when("I create a PackageContent with the id and content")
|
||||
def step_when_create_content_id_content(context: Context) -> None:
|
||||
context.package_content = PackageContent(
|
||||
id=context.package_id, content=context.content_dict
|
||||
)
|
||||
|
||||
|
||||
@when('I create a PackageContent with id, content, and original_reference "{ref}"')
|
||||
def step_when_create_content_with_ref(context: Context, ref: str) -> None:
|
||||
context.package_content = PackageContent(
|
||||
id=context.package_id,
|
||||
content=context.content_dict,
|
||||
original_reference=ref,
|
||||
)
|
||||
|
||||
|
||||
@then('the PackageContent id should be "{expected}"')
|
||||
def step_then_content_id(context: Context, expected: str) -> None:
|
||||
assert context.package_content.id.id_string == expected, (
|
||||
f"Expected id {expected!r}, got {context.package_content.id.id_string!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the PackageContent content should have key "{key}" with value "{value}"')
|
||||
def step_then_content_value(context: Context, key: str, value: str) -> None:
|
||||
assert context.package_content.content.get(key) == value, (
|
||||
f"Expected content[{key!r}]={value!r}, "
|
||||
f"got {context.package_content.content.get(key)!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the PackageContent fetched_at should be set")
|
||||
def step_then_content_fetched_at(context: Context) -> None:
|
||||
assert isinstance(context.package_content.fetched_at, datetime), (
|
||||
"fetched_at should be a datetime"
|
||||
)
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
delta = now - context.package_content.fetched_at
|
||||
assert delta.total_seconds() < 10, "fetched_at should be recent"
|
||||
|
||||
|
||||
@then('the PackageContent original_reference should be "{expected}"')
|
||||
def step_then_content_original_ref(context: Context, expected: str) -> None:
|
||||
assert context.package_content.original_reference == expected, (
|
||||
f"Expected original_reference {expected!r}, "
|
||||
f"got {context.package_content.original_reference!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the PackageContent should be hashable")
|
||||
def step_then_content_hashable(context: Context) -> None:
|
||||
_ = hash(context.package_content)
|
||||
used_as_key = {context.package_content: "value"}
|
||||
assert used_as_key[context.package_content] == "value"
|
||||
@@ -109,11 +109,15 @@ class CleverActorsLib: # pragma: no cover - integration test library
|
||||
overlay = {overlay_key: overlay_val}
|
||||
result = merge_configs(base, overlay)
|
||||
if len(result) != 2:
|
||||
raise AssertionError(f"Expected result length 2, got {len(result)}: {result!r}")
|
||||
raise AssertionError(
|
||||
f"Expected result length 2, got {len(result)}: {result!r}"
|
||||
)
|
||||
if result.get(base_key) != base_val:
|
||||
raise AssertionError(f"Expected {base_key}={base_val!r}, got {result!r}")
|
||||
if result.get(overlay_key) != overlay_val:
|
||||
raise AssertionError(f"Expected {overlay_key}={overlay_val!r}, got {result!r}")
|
||||
raise AssertionError(
|
||||
f"Expected {overlay_key}={overlay_val!r}, got {result!r}"
|
||||
)
|
||||
# Verify inputs not mutated
|
||||
if base != {base_key: base_val}:
|
||||
raise AssertionError(f"Base dict was mutated: {base!r}")
|
||||
@@ -135,7 +139,9 @@ class CleverActorsLib: # pragma: no cover - integration test library
|
||||
if result != {"items": [1, 2, 3, 4]}:
|
||||
raise AssertionError(f"Expected items=[1,2,3,4], got {result!r}")
|
||||
|
||||
def merge_configs_through_config_pipeline(self, base_key: str, overlay_val: str) -> None:
|
||||
def merge_configs_through_config_pipeline(
|
||||
self, base_key: str, overlay_val: str
|
||||
) -> None:
|
||||
"""End-to-end test: load YAML via ConfigurationManager, merge with overlay.
|
||||
|
||||
Loads the test_config.yaml fixture through the canonical config-loading
|
||||
@@ -167,9 +173,7 @@ class CleverActorsLib: # pragma: no cover - integration test library
|
||||
f"Base fixture key 'agents' lost during merge: {sorted(result.keys())}"
|
||||
)
|
||||
if "cleveragents" not in result:
|
||||
raise AssertionError(
|
||||
f"Base fixture key 'cleveragents' lost during merge"
|
||||
)
|
||||
raise AssertionError(f"Base fixture key 'cleveragents' lost during merge")
|
||||
|
||||
def schema_validator_accepts_minimum_config(self) -> None:
|
||||
config = {
|
||||
|
||||
@@ -36,9 +36,7 @@ class ValidateDictLib: # pragma: no cover - integration test library
|
||||
"Expected ConfigurationError but validate_dict returned without error"
|
||||
)
|
||||
|
||||
def validate_dict_returns_same_object(
|
||||
self, config_dict: dict[str, Any]
|
||||
) -> None:
|
||||
def validate_dict_returns_same_object(self, config_dict: dict[str, Any]) -> None:
|
||||
"""Assert that validate_dict returns the same dict object (identity)."""
|
||||
result = validate_dict(config_dict, {})
|
||||
if result is not config_dict:
|
||||
@@ -67,4 +65,4 @@ class ValidateDictLib: # pragma: no cover - integration test library
|
||||
raise AssertionError(
|
||||
f"'ConfigurationError' not found in cleveractors.__all__: "
|
||||
f"{cleveractors.__all__}"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Package Registry client implementing the Package Registry Standard v1.0.0."""
|
||||
"""Package Registry client and core types (Package Registry Standard v1.0.0)."""
|
||||
|
||||
from cleveractors.registry.client import RegistryClient
|
||||
from cleveractors.registry.exceptions import (
|
||||
@@ -13,6 +13,13 @@ from cleveractors.registry.exceptions import (
|
||||
ValidationError,
|
||||
VersionNotFoundError,
|
||||
)
|
||||
from cleveractors.registry.types import (
|
||||
PackageContent,
|
||||
PackageId,
|
||||
PackageReference,
|
||||
PackageType,
|
||||
ReferenceType,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"RegistryClient",
|
||||
@@ -26,4 +33,9 @@ __all__ = [
|
||||
"ConflictError",
|
||||
"InternalServerError",
|
||||
"RegistryNetworkError",
|
||||
"PackageContent",
|
||||
"PackageId",
|
||||
"PackageReference",
|
||||
"PackageType",
|
||||
"ReferenceType",
|
||||
]
|
||||
|
||||
@@ -25,17 +25,6 @@ from cleveractors.registry.exceptions import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PACKAGE_TYPE_PREFIXES: dict[str, str] = {
|
||||
"actor": "pkg_act_",
|
||||
"graph": "pkg_grh_",
|
||||
"stream": "pkg_str_",
|
||||
"agent": "pkg_agt_",
|
||||
"template": "pkg_tpl_",
|
||||
"skill": "pkg_skl_",
|
||||
"mcp": "pkg_mcp_",
|
||||
"lsp": "pkg_lsp_",
|
||||
}
|
||||
|
||||
|
||||
class RegistryClient:
|
||||
"""Async HTTP client for the Package Registry Standard v1.0.0.
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Core data types for the Package Registry (Package Registry Standard v1.0.0)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from cleveractors.registry.exceptions import InvalidPackageIdError
|
||||
|
||||
|
||||
class PackageType(Enum):
|
||||
"""Package types per Package Registry Standard §3.2."""
|
||||
|
||||
ACTOR = "act"
|
||||
GRAPH = "grh"
|
||||
STREAM = "str"
|
||||
AGENT = "agt"
|
||||
TEMPLATE = "tpl"
|
||||
SKILL = "skl"
|
||||
MCP = "mcp"
|
||||
LSP = "lsp"
|
||||
|
||||
|
||||
class ReferenceType(Enum):
|
||||
"""Reference formats per Package Registry Standard §5.3."""
|
||||
|
||||
REGISTRY = "registry"
|
||||
ID = "id"
|
||||
LOCAL = "local"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageId:
|
||||
"""Content-addressed package identifier (Package Registry Standard §5.1).
|
||||
|
||||
Format: ``pkg_<type>_<40-hex-sha1>``.
|
||||
"""
|
||||
|
||||
package_type: PackageType
|
||||
sha1_hex: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if len(self.sha1_hex) != 40:
|
||||
raise InvalidPackageIdError(
|
||||
f"SHA1 must be exactly 40 hex characters, "
|
||||
f"got {len(self.sha1_hex)}: {self.sha1_hex!r}"
|
||||
)
|
||||
if not all(c in "0123456789abcdef" for c in self.sha1_hex):
|
||||
raise InvalidPackageIdError(
|
||||
f"SHA1 must contain only lowercase hex characters: {self.sha1_hex!r}"
|
||||
)
|
||||
|
||||
@property
|
||||
def id_string(self) -> str:
|
||||
"""The full ``pkg_<type>_<sha1>`` string."""
|
||||
return f"pkg_{self.package_type.value}_{self.sha1_hex}"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, raw_id: str) -> PackageId:
|
||||
"""Parse and validate a Package ID string.
|
||||
|
||||
Args:
|
||||
raw_id: A string in ``pkg_<type>_<40-hex-sha1>`` format.
|
||||
|
||||
Returns:
|
||||
A validated PackageId instance.
|
||||
|
||||
Raises:
|
||||
InvalidPackageIdError: If the string does not match the required format.
|
||||
"""
|
||||
raw_id = raw_id.strip()
|
||||
if not raw_id:
|
||||
raise InvalidPackageIdError("Package ID must not be empty")
|
||||
|
||||
if not raw_id.startswith("pkg_"):
|
||||
raise InvalidPackageIdError(
|
||||
f"Package ID must start with 'pkg_', got: {raw_id!r}"
|
||||
)
|
||||
|
||||
rest = raw_id[4:]
|
||||
|
||||
matched_type: PackageType | None = None
|
||||
for pkg_type in PackageType:
|
||||
candidate = pkg_type.value + "_"
|
||||
if rest.startswith(candidate):
|
||||
matched_type = pkg_type
|
||||
break
|
||||
|
||||
if matched_type is None:
|
||||
valid_prefixes = ", ".join(t.value for t in PackageType)
|
||||
raise InvalidPackageIdError(
|
||||
f"Unknown package type prefix in {raw_id!r}. "
|
||||
f"Valid prefixes: {valid_prefixes}"
|
||||
)
|
||||
|
||||
sha1_start = len(matched_type.value) + 1
|
||||
return cls(package_type=matched_type, sha1_hex=rest[sha1_start:])
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.id_string
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"PackageId({self.id_string!r})"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageReference:
|
||||
"""A reference that resolves to a concrete PackageId (§5.3).
|
||||
|
||||
Holds the original user-supplied reference string for debugging
|
||||
alongside parsed fields.
|
||||
|
||||
Formats:
|
||||
- Registry: ``server:ns/name@version``
|
||||
- ID: ``ID:pkg_<type>_<40-hex-sha1>``
|
||||
- Local: ``local:<path>``
|
||||
"""
|
||||
|
||||
original_reference: str
|
||||
reference_type: ReferenceType
|
||||
server: str | None = None
|
||||
namespace: str | None = None
|
||||
name: str | None = None
|
||||
version: str | None = None
|
||||
id_string: str | None = None
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, raw_ref: str) -> PackageReference:
|
||||
"""Parse a package reference string.
|
||||
|
||||
Args:
|
||||
raw_ref: A reference string in one of the three supported formats.
|
||||
|
||||
Returns:
|
||||
A PackageReference with parsed fields.
|
||||
|
||||
Raises:
|
||||
ValueError: If the reference string cannot be parsed.
|
||||
"""
|
||||
raw_ref = raw_ref.strip()
|
||||
if not raw_ref:
|
||||
raise ValueError("Package reference string must not be empty")
|
||||
|
||||
if raw_ref.startswith("ID:"):
|
||||
raw_id = raw_ref[3:]
|
||||
if not raw_id:
|
||||
raise ValueError("ID reference must contain a Package ID")
|
||||
try:
|
||||
PackageId.from_string(raw_id)
|
||||
except (ValueError, InvalidPackageIdError) as exc:
|
||||
raise ValueError(f"Invalid ID reference: {exc}") from exc
|
||||
return cls(
|
||||
original_reference=raw_ref,
|
||||
reference_type=ReferenceType.ID,
|
||||
id_string=raw_id,
|
||||
)
|
||||
|
||||
if raw_ref.startswith("local:"):
|
||||
local_path = raw_ref[6:]
|
||||
if not local_path:
|
||||
raise ValueError("LOCAL reference must have a path after 'local:'")
|
||||
return cls(
|
||||
original_reference=raw_ref,
|
||||
reference_type=ReferenceType.LOCAL,
|
||||
name=local_path,
|
||||
)
|
||||
|
||||
if ":" not in raw_ref:
|
||||
raise ValueError(
|
||||
"Registry reference must be in format 'server:ns/name[@version]'"
|
||||
)
|
||||
|
||||
server, rest = raw_ref.split(":", 1)
|
||||
if not server:
|
||||
raise ValueError("Registry reference must include a server name")
|
||||
|
||||
if "@" in rest:
|
||||
ns_name, version = rest.rsplit("@", 1)
|
||||
if not version:
|
||||
version = "latest"
|
||||
else:
|
||||
ns_name, version = rest, "latest"
|
||||
|
||||
if "/" not in ns_name:
|
||||
raise ValueError(
|
||||
"Registry reference must include namespace/name after the server"
|
||||
)
|
||||
|
||||
namespace, name = ns_name.split("/", 1)
|
||||
|
||||
if not namespace:
|
||||
raise ValueError("Registry reference must include a namespace")
|
||||
if not name:
|
||||
raise ValueError("Registry reference must include a name")
|
||||
|
||||
return cls(
|
||||
original_reference=raw_ref,
|
||||
reference_type=ReferenceType.REGISTRY,
|
||||
server=server,
|
||||
namespace=namespace,
|
||||
name=name,
|
||||
version=version,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.original_reference
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"PackageReference({self.original_reference!r})"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PackageContent:
|
||||
"""A fetched package document with metadata.
|
||||
|
||||
Wraps the raw content dictionary alongside its resolved identity
|
||||
and the original reference supplied by the caller.
|
||||
"""
|
||||
|
||||
id: PackageId
|
||||
content: dict[str, Any] = field(hash=False, compare=False)
|
||||
original_reference: str | None = None
|
||||
fetched_at: datetime = field(
|
||||
default_factory=lambda: datetime.now(tz=timezone.utc), hash=False
|
||||
)
|
||||
Reference in New Issue
Block a user