240 lines
6.5 KiB
Python
240 lines
6.5 KiB
Python
|
|
"""
|
||
|
|
Artifact service for high-level artifact management operations.
|
||
|
|
|
||
|
|
This service provides business logic for creating, querying, and updating
|
||
|
|
artifacts with automatic content digest calculation and change tracking.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from typing import List, Optional
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
from markitect.prompts.models import (
|
||
|
|
Artifact,
|
||
|
|
ArtifactType,
|
||
|
|
ArtifactMetadata,
|
||
|
|
ArtifactReference,
|
||
|
|
calculate_content_digest,
|
||
|
|
)
|
||
|
|
from markitect.prompts.repositories.interfaces import (
|
||
|
|
IArtifactRepository,
|
||
|
|
ArtifactNotFoundError,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class ArtifactService:
|
||
|
|
"""
|
||
|
|
Service for artifact management operations.
|
||
|
|
|
||
|
|
Provides high-level business logic on top of artifact repository,
|
||
|
|
handling content digest calculation, change detection, and cross-space
|
||
|
|
artifact resolution.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, repository: IArtifactRepository):
|
||
|
|
"""
|
||
|
|
Initialize service with repository.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
repository: Artifact repository implementation
|
||
|
|
"""
|
||
|
|
self.repository = repository
|
||
|
|
|
||
|
|
def create_artifact(
|
||
|
|
self,
|
||
|
|
space_id: str,
|
||
|
|
name: str,
|
||
|
|
content: str,
|
||
|
|
artifact_type: ArtifactType = ArtifactType.CONTENT,
|
||
|
|
metadata: Optional[ArtifactMetadata] = None,
|
||
|
|
) -> Artifact:
|
||
|
|
"""
|
||
|
|
Create a new artifact with automatic digest calculation.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
space_id: ID of containing space
|
||
|
|
name: Artifact name
|
||
|
|
content: Artifact content
|
||
|
|
artifact_type: Type classification
|
||
|
|
metadata: Optional metadata
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Created artifact
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
DuplicateArtifactError: If artifact already exists
|
||
|
|
"""
|
||
|
|
artifact = Artifact.create(
|
||
|
|
space_id=space_id,
|
||
|
|
name=name,
|
||
|
|
content=content,
|
||
|
|
artifact_type=artifact_type,
|
||
|
|
metadata=metadata,
|
||
|
|
)
|
||
|
|
return self.repository.create(artifact)
|
||
|
|
|
||
|
|
def get_artifact(self, artifact_id: str) -> Artifact:
|
||
|
|
"""
|
||
|
|
Retrieve artifact by ID.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
artifact_id: Artifact identifier
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Artifact instance
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ArtifactNotFoundError: If artifact doesn't exist
|
||
|
|
"""
|
||
|
|
artifact = self.repository.get_by_id(artifact_id)
|
||
|
|
if not artifact:
|
||
|
|
raise ArtifactNotFoundError(f"Artifact with ID '{artifact_id}' not found")
|
||
|
|
return artifact
|
||
|
|
|
||
|
|
def get_artifact_by_name(self, space_id: str, name: str) -> Artifact:
|
||
|
|
"""
|
||
|
|
Retrieve artifact by space and name.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
space_id: Space identifier
|
||
|
|
name: Artifact name
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Artifact instance
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ArtifactNotFoundError: If artifact doesn't exist
|
||
|
|
"""
|
||
|
|
artifact = self.repository.get_by_name(space_id, name)
|
||
|
|
if not artifact:
|
||
|
|
raise ArtifactNotFoundError(
|
||
|
|
f"Artifact '{name}' not found in space '{space_id}'"
|
||
|
|
)
|
||
|
|
return artifact
|
||
|
|
|
||
|
|
def resolve_reference(
|
||
|
|
self,
|
||
|
|
reference: ArtifactReference,
|
||
|
|
search_spaces: List[str],
|
||
|
|
) -> Optional[Artifact]:
|
||
|
|
"""
|
||
|
|
Resolve an artifact reference across multiple spaces.
|
||
|
|
|
||
|
|
Implements FR-1.3: Cross-space artifact references
|
||
|
|
Searches spaces in order until artifact is found.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
reference: Artifact reference to resolve
|
||
|
|
search_spaces: List of space IDs to search in order
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Resolved artifact, or None if not found
|
||
|
|
"""
|
||
|
|
# If reference specifies space, search only that space
|
||
|
|
if reference.space_id:
|
||
|
|
return self.repository.get_by_name(reference.space_id, reference.name)
|
||
|
|
|
||
|
|
# Search spaces in order
|
||
|
|
for space_id in search_spaces:
|
||
|
|
artifact = self.repository.get_by_name(space_id, reference.name)
|
||
|
|
if artifact:
|
||
|
|
# TODO: Handle version constraint when versioning is implemented
|
||
|
|
return artifact
|
||
|
|
|
||
|
|
return None
|
||
|
|
|
||
|
|
def update_artifact_content(
|
||
|
|
self,
|
||
|
|
artifact_id: str,
|
||
|
|
new_content: str,
|
||
|
|
) -> Artifact:
|
||
|
|
"""
|
||
|
|
Update artifact content with automatic digest recalculation.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
artifact_id: Artifact to update
|
||
|
|
new_content: New content
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Updated artifact
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ArtifactNotFoundError: If artifact doesn't exist
|
||
|
|
"""
|
||
|
|
artifact = self.get_artifact(artifact_id)
|
||
|
|
artifact.update_content(new_content)
|
||
|
|
return self.repository.update(artifact)
|
||
|
|
|
||
|
|
def detect_change(self, artifact_id: str, current_content: str) -> bool:
|
||
|
|
"""
|
||
|
|
Detect if artifact content has changed.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
artifact_id: Artifact to check
|
||
|
|
current_content: Current content to compare
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if content has changed
|
||
|
|
|
||
|
|
Raises:
|
||
|
|
ArtifactNotFoundError: If artifact doesn't exist
|
||
|
|
"""
|
||
|
|
artifact = self.get_artifact(artifact_id)
|
||
|
|
current_digest = calculate_content_digest(current_content)
|
||
|
|
return artifact.has_changed(current_digest)
|
||
|
|
|
||
|
|
def list_artifacts(
|
||
|
|
self,
|
||
|
|
space_id: str,
|
||
|
|
artifact_type: Optional[ArtifactType] = None,
|
||
|
|
) -> List[Artifact]:
|
||
|
|
"""
|
||
|
|
List artifacts in a space.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
space_id: Space identifier
|
||
|
|
artifact_type: Optional type filter
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of artifacts
|
||
|
|
"""
|
||
|
|
return self.repository.list_by_space(space_id, artifact_type)
|
||
|
|
|
||
|
|
def find_by_digest(self, content_digest: str) -> List[Artifact]:
|
||
|
|
"""
|
||
|
|
Find all artifacts with matching content digest.
|
||
|
|
|
||
|
|
Useful for finding duplicate content across spaces.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
content_digest: SHA-256 digest to match
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
List of artifacts with matching digest
|
||
|
|
"""
|
||
|
|
return self.repository.get_by_digest(content_digest)
|
||
|
|
|
||
|
|
def delete_artifact(self, artifact_id: str) -> bool:
|
||
|
|
"""
|
||
|
|
Delete an artifact.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
artifact_id: Artifact to delete
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if deleted, False if not found
|
||
|
|
"""
|
||
|
|
return self.repository.delete(artifact_id)
|
||
|
|
|
||
|
|
def artifact_exists(self, space_id: str, name: str) -> bool:
|
||
|
|
"""
|
||
|
|
Check if artifact exists.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
space_id: Space identifier
|
||
|
|
name: Artifact name
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if artifact exists
|
||
|
|
"""
|
||
|
|
return self.repository.exists(space_id, name)
|