almanac-contracts 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: almanac-contracts
3
+ Version: 0.5.0
4
+ Summary: Shared wire contracts for Almanac clients and services.
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pydantic>=2.8
@@ -0,0 +1,14 @@
1
+ [project]
2
+ name = "almanac-contracts"
3
+ version = "0.5.0"
4
+ description = "Shared wire contracts for Almanac clients and services."
5
+ requires-python = ">=3.12"
6
+ dependencies = ["pydantic>=2.8"]
7
+
8
+ [build-system]
9
+ requires = ["setuptools>=77"]
10
+ build-backend = "setuptools.build_meta"
11
+
12
+ [tool.setuptools.packages.find]
13
+ where = ["src"]
14
+ include = ["almanac_contracts*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Stable contracts shared by Almanac's public edges."""
@@ -0,0 +1,65 @@
1
+ from typing import Literal
2
+
3
+ ErrorCode = Literal[
4
+ "almanac_error",
5
+ "not_found",
6
+ "conflict",
7
+ "not_authenticated",
8
+ "forbidden",
9
+ "validation_failed",
10
+ "provider_unavailable",
11
+ ]
12
+
13
+
14
+ class AlmanacError(Exception):
15
+ """Base class for product errors that can cross CLI and server edges."""
16
+
17
+ code: ErrorCode = "almanac_error"
18
+
19
+ def __init__(self, message: str, *, request_id: str | None = None):
20
+ super().__init__(message)
21
+ self.request_id = request_id
22
+
23
+
24
+ class NotFoundError(AlmanacError):
25
+ code: ErrorCode = "not_found"
26
+
27
+ def __init__(self, resource: str, identifier: str):
28
+ super().__init__(f"{resource} not found: {identifier}")
29
+ self.resource = resource
30
+ self.identifier = identifier
31
+
32
+
33
+ class ConflictError(AlmanacError):
34
+ code: ErrorCode = "conflict"
35
+
36
+ def __init__(self, message: str):
37
+ super().__init__(message)
38
+
39
+
40
+ class ValidationFailed(AlmanacError):
41
+ code: ErrorCode = "validation_failed"
42
+
43
+ def __init__(self, message: str):
44
+ super().__init__(message)
45
+
46
+
47
+ class NotAuthenticated(AlmanacError):
48
+ code: ErrorCode = "not_authenticated"
49
+
50
+ def __init__(self, message: str = "Missing or invalid credentials"):
51
+ super().__init__(message)
52
+
53
+
54
+ class ForbiddenError(AlmanacError):
55
+ code: ErrorCode = "forbidden"
56
+
57
+ def __init__(self, message: str = "Insufficient permission"):
58
+ super().__init__(message)
59
+
60
+
61
+ class ProviderUnavailable(AlmanacError):
62
+ code: ErrorCode = "provider_unavailable"
63
+
64
+ def __init__(self, message: str):
65
+ super().__init__(message)
@@ -0,0 +1,77 @@
1
+ from almanac_contracts.hosted.api_keys import ApiKey, CreatedApiKey
2
+ from almanac_contracts.hosted.billing import (
3
+ BillingPlanChange,
4
+ BillingPortal,
5
+ BillingPreview,
6
+ BillingState,
7
+ )
8
+ from almanac_contracts.hosted.jobs import (
9
+ Job,
10
+ JobEvent,
11
+ JobImportOutcome,
12
+ JobOutcome,
13
+ JobPageChange,
14
+ JobSourceChange,
15
+ )
16
+ from almanac_contracts.hosted.pages import (
17
+ Page,
18
+ PageRecord,
19
+ PageSearchResult,
20
+ PageSource,
21
+ PageSummary,
22
+ )
23
+ from almanac_contracts.hosted.sources import (
24
+ ImportItem,
25
+ Source,
26
+ SourceFolder,
27
+ SourceTree,
28
+ Upload,
29
+ UploadFilePlan,
30
+ UploadPlan,
31
+ UploadResult,
32
+ UploadTarget,
33
+ )
34
+ from almanac_contracts.hosted.topics import Topic, TopicSummary
35
+ from almanac_contracts.hosted.wikis import Wiki
36
+ from almanac_contracts.hosted.workspaces import (
37
+ Workspace,
38
+ WorkspaceInvitation,
39
+ WorkspaceMember,
40
+ WorkspaceMembers,
41
+ )
42
+
43
+ __all__ = [
44
+ "ApiKey",
45
+ "BillingPlanChange",
46
+ "BillingPortal",
47
+ "BillingPreview",
48
+ "BillingState",
49
+ "CreatedApiKey",
50
+ "Job",
51
+ "JobEvent",
52
+ "JobImportOutcome",
53
+ "JobOutcome",
54
+ "JobPageChange",
55
+ "JobSourceChange",
56
+ "ImportItem",
57
+ "Page",
58
+ "PageRecord",
59
+ "PageSearchResult",
60
+ "PageSource",
61
+ "PageSummary",
62
+ "Source",
63
+ "SourceFolder",
64
+ "SourceTree",
65
+ "Topic",
66
+ "TopicSummary",
67
+ "Upload",
68
+ "UploadFilePlan",
69
+ "UploadPlan",
70
+ "UploadResult",
71
+ "UploadTarget",
72
+ "Wiki",
73
+ "Workspace",
74
+ "WorkspaceInvitation",
75
+ "WorkspaceMember",
76
+ "WorkspaceMembers",
77
+ ]
@@ -0,0 +1,18 @@
1
+ from pydantic import Field
2
+
3
+ from almanac_contracts.models import AlmanacModel
4
+
5
+
6
+ class ApiKey(AlmanacModel):
7
+ api_key_id: str
8
+ name: str
9
+ obfuscated_value: str
10
+ permissions: list[str] = Field(default_factory=list)
11
+ created_at: str
12
+ updated_at: str
13
+ last_used_at: str | None = None
14
+ expires_at: str | None = None
15
+
16
+
17
+ class CreatedApiKey(ApiKey):
18
+ value: str
@@ -0,0 +1,71 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field
4
+
5
+ from almanac_contracts.models import AlmanacModel
6
+
7
+
8
+ class PlanFeature(AlmanacModel):
9
+ feature_id: str
10
+ included: float | None = None
11
+
12
+
13
+ class BillingUsage(AlmanacModel):
14
+ feature_id: str
15
+ granted: float
16
+ remaining: float
17
+ usage: float
18
+ unlimited: bool = False
19
+ overage_allowed: bool = False
20
+ next_reset_at: float | None = None
21
+
22
+
23
+ class BillingPlan(AlmanacModel):
24
+ plan_id: str
25
+ name: str
26
+ group: str | None = None
27
+ price: float | None = None
28
+ interval: str | None = None
29
+ action: (
30
+ Literal["activate", "upgrade", "downgrade", "purchase", "none"] | None
31
+ ) = None
32
+ status: Literal["active", "scheduled"] | None = None
33
+ features: list[PlanFeature] = Field(default_factory=list)
34
+
35
+
36
+ class BillingManage(AlmanacModel):
37
+ allowed: bool
38
+ reason: Literal["permission", "not_allowed"]
39
+
40
+
41
+ class BillingState(AlmanacModel):
42
+ workspace_id: str
43
+ manage: BillingManage
44
+ plans: list[BillingPlan]
45
+ usage: list[BillingUsage] = Field(default_factory=list)
46
+
47
+
48
+ class BillingPortal(AlmanacModel):
49
+ url: str
50
+
51
+
52
+ class BillingPlanChange(AlmanacModel):
53
+ redirect_url: str | None = None
54
+
55
+
56
+ class BillingPreviewLine(AlmanacModel):
57
+ name: str
58
+ description: str | None = None
59
+ subtotal: float | None = None
60
+ total: float | None = None
61
+ quantity: float | None = None
62
+
63
+
64
+ class BillingPreview(AlmanacModel):
65
+ plan_id: str
66
+ currency: str | None = None
67
+ subtotal: float | None = None
68
+ total: float | None = None
69
+ redirect_to_checkout: bool = False
70
+ checkout_type: str | None = None
71
+ line_items: list[BillingPreviewLine] = Field(default_factory=list)
@@ -0,0 +1,63 @@
1
+ from typing import Literal
2
+
3
+ from almanac_contracts.hosted.status import (
4
+ ImportStatus,
5
+ JobEventLevel,
6
+ JobKind,
7
+ JobStatus,
8
+ JobStatusState,
9
+ )
10
+ from almanac_contracts.models import AlmanacModel
11
+
12
+
13
+ class Job(AlmanacModel):
14
+ job_id: str
15
+ wiki_id: str
16
+ kind: JobKind
17
+ status: JobStatus
18
+ title: str | None
19
+ current_step: str | None
20
+ summary: str | None
21
+ error: str | None
22
+ status_state: JobStatusState
23
+ status_label: str
24
+ created_at: str
25
+ updated_at: str
26
+ started_at: str | None
27
+ finished_at: str | None
28
+
29
+
30
+ class JobEvent(AlmanacModel):
31
+ event_id: str
32
+ job_id: str
33
+ sequence: int
34
+ level: JobEventLevel
35
+ message: str
36
+ created_at: str
37
+ payload_json: dict[str, object] | None = None
38
+
39
+
40
+ class JobImportOutcome(AlmanacModel):
41
+ original_path: str
42
+ status: ImportStatus
43
+ source_path: str | None
44
+ error: str | None
45
+
46
+
47
+ class JobSourceChange(AlmanacModel):
48
+ action: Literal["added", "moved"]
49
+ path: str
50
+ previous_path: str | None
51
+
52
+
53
+ class JobPageChange(AlmanacModel):
54
+ action: Literal["created", "updated", "removed"]
55
+ path: str
56
+ title: str
57
+
58
+
59
+ class JobOutcome(AlmanacModel):
60
+ job: Job
61
+ imports: list[JobImportOutcome]
62
+ sources: list[JobSourceChange]
63
+ pages: list[JobPageChange]
@@ -0,0 +1,43 @@
1
+ from almanac_contracts.hosted.status import PageStatus, StatusState
2
+ from almanac_contracts.models import AlmanacModel
3
+
4
+
5
+ class PageRecord(AlmanacModel):
6
+ page_id: str
7
+ wiki_id: str
8
+ path: str
9
+ title: str
10
+ status: PageStatus
11
+ updated_at: str
12
+
13
+
14
+ class PageSummary(PageRecord):
15
+ folder: str
16
+ name: str
17
+ status_state: StatusState
18
+ status_label: str
19
+
20
+
21
+ class PageSearchResult(PageSummary):
22
+ summary: str
23
+ rank: float
24
+ excerpt: str
25
+
26
+
27
+ class Page(PageSummary):
28
+ file_path: str
29
+ summary: str
30
+ body_md: str
31
+ frontmatter_json: dict[str, object]
32
+
33
+
34
+ class PageSource(AlmanacModel):
35
+ page_id: str
36
+ source_id: str
37
+ key: str
38
+ title: str
39
+ job_id: str | None
40
+ note: str
41
+ path: str
42
+ original_filename: str
43
+ created_at: str
@@ -0,0 +1,82 @@
1
+ from almanac_contracts.hosted.jobs import Job
2
+ from almanac_contracts.hosted.status import ImportStatus, StatusState, UploadStatus
3
+ from almanac_contracts.models import AlmanacModel
4
+
5
+
6
+ class Source(AlmanacModel):
7
+ source_id: str
8
+ wiki_id: str
9
+ path: str
10
+ folder: str
11
+ name: str
12
+ original_filename: str
13
+ mime_type: str | None
14
+ size_bytes: int
15
+ sha256: str
16
+ created_at: str
17
+ updated_at: str
18
+
19
+
20
+ class Upload(AlmanacModel):
21
+ upload_id: str
22
+ wiki_id: str
23
+ status: UploadStatus
24
+ status_state: StatusState
25
+ status_label: str
26
+ source_count: int
27
+ summary: str | None
28
+ error: str | None
29
+ created_at: str
30
+ updated_at: str
31
+ finished_at: str | None
32
+
33
+
34
+ class ImportItem(AlmanacModel):
35
+ import_id: str
36
+ original_path: str
37
+ name: str
38
+ status: ImportStatus
39
+ status_state: StatusState
40
+ status_label: str
41
+ source_id: str | None
42
+ duplicate_source_id: str | None
43
+ error: str | None
44
+ mime_type: str | None
45
+ size_bytes: int
46
+ sha256: str
47
+
48
+
49
+ class SourceFolder(AlmanacModel):
50
+ path: str
51
+ name: str
52
+ parent: str | None
53
+ source_count: int
54
+
55
+
56
+ class SourceTree(AlmanacModel):
57
+ folders: list[SourceFolder]
58
+ sources: list[Source]
59
+
60
+
61
+ class UploadResult(AlmanacModel):
62
+ upload: Upload
63
+ imports: list[ImportItem]
64
+ job: Job | None
65
+
66
+
67
+ class UploadTarget(AlmanacModel):
68
+ method: str
69
+ url: str
70
+ headers: dict[str, str]
71
+ expires_at: str
72
+
73
+
74
+ class UploadFilePlan(AlmanacModel):
75
+ file_id: str
76
+ path: str
77
+ target: UploadTarget | None
78
+
79
+
80
+ class UploadPlan(AlmanacModel):
81
+ upload: Upload
82
+ files: list[UploadFilePlan]
@@ -0,0 +1,10 @@
1
+ from typing import Literal
2
+
3
+ JobKind = Literal["ingest", "garden"]
4
+ JobStatus = Literal["queued", "running", "succeeded", "failed"]
5
+ JobEventLevel = Literal["info", "warning", "error"]
6
+ ImportStatus = Literal["pending", "organized", "duplicate", "failed"]
7
+ PageStatus = Literal["active", "archived"]
8
+ UploadStatus = Literal["uploading", "queued", "completed", "failed"]
9
+ StatusState = Literal["ready", "running", "failed", "unknown"]
10
+ JobStatusState = Literal["queued", "running", "succeeded", "failed", "unknown"]
@@ -0,0 +1,12 @@
1
+ from almanac_contracts.hosted.pages import PageRecord
2
+ from almanac_contracts.models import AlmanacModel
3
+
4
+
5
+ class TopicSummary(AlmanacModel):
6
+ topic: str
7
+ page_count: int
8
+
9
+
10
+ class Topic(AlmanacModel):
11
+ topic: str
12
+ pages: list[PageRecord]
@@ -0,0 +1,9 @@
1
+ from almanac_contracts.models import AlmanacModel
2
+
3
+
4
+ class Wiki(AlmanacModel):
5
+ wiki_id: str
6
+ workspace_id: str
7
+ slug: str
8
+ title: str
9
+ description: str = ""
@@ -0,0 +1,63 @@
1
+ from pydantic import Field
2
+
3
+ from almanac_contracts.models import AlmanacModel
4
+
5
+
6
+ class Workspace(AlmanacModel):
7
+ workspace_id: str
8
+ name: str
9
+ slug: str
10
+ role: str
11
+ permissions: list[str] = Field(default_factory=list)
12
+
13
+
14
+ class WorkspaceAccess(AlmanacModel):
15
+ workspace_id: str
16
+ role: str
17
+ can_manage_members: bool = False
18
+ can_manage_billing: bool = False
19
+ can_manage_developers: bool = False
20
+
21
+
22
+ class WorkspaceMember(AlmanacModel):
23
+ membership_id: str
24
+ user_id: str
25
+ email: str
26
+ display_name: str
27
+ avatar_url: str = ""
28
+ role: str
29
+ status: str = "active"
30
+
31
+
32
+ class WorkspaceInvitation(AlmanacModel):
33
+ invitation_id: str
34
+ email: str
35
+ role: str
36
+ state: str = "pending"
37
+ expires_at: str | None = None
38
+ created_at: str | None = None
39
+
40
+
41
+ class WorkspaceRole(AlmanacModel):
42
+ slug: str
43
+ name: str
44
+ description: str = ""
45
+
46
+
47
+ class WorkspaceSeats(AlmanacModel):
48
+ active: int
49
+ pending: int = 0
50
+ used: int
51
+ granted: float | None = None
52
+ unlimited: bool = False
53
+ overage_allowed: bool = False
54
+ blocked: bool = False
55
+
56
+
57
+ class WorkspaceMembers(AlmanacModel):
58
+ workspace_id: str
59
+ viewer: WorkspaceAccess
60
+ members: list[WorkspaceMember] = Field(default_factory=list)
61
+ invitations: list[WorkspaceInvitation] = Field(default_factory=list)
62
+ roles: list[WorkspaceRole] = Field(default_factory=list)
63
+ seats: WorkspaceSeats
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, ConfigDict, Field
2
+
3
+
4
+ class AlmanacModel(BaseModel):
5
+ model_config = ConfigDict(frozen=True, extra="forbid")
6
+
7
+
8
+ class PageCursor(AlmanacModel):
9
+ limit: int = Field(default=50, ge=1, le=500)
10
+ offset: int = Field(default=0, ge=0)
@@ -0,0 +1,16 @@
1
+ import re
2
+
3
+ from almanac_contracts.errors import ValidationFailed
4
+
5
+ HOSTNAME = re.compile(
6
+ r"(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*"
7
+ r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?",
8
+ re.IGNORECASE,
9
+ )
10
+
11
+
12
+ def required_hostname(value: str, name: str) -> str:
13
+ hostname = value.strip()
14
+ if not HOSTNAME.fullmatch(hostname):
15
+ raise ValidationFailed(f"{name} must be a hostname")
16
+ return hostname.lower()
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: almanac-contracts
3
+ Version: 0.5.0
4
+ Summary: Shared wire contracts for Almanac clients and services.
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pydantic>=2.8
@@ -0,0 +1,20 @@
1
+ pyproject.toml
2
+ src/almanac_contracts/__init__.py
3
+ src/almanac_contracts/errors.py
4
+ src/almanac_contracts/models.py
5
+ src/almanac_contracts/urls.py
6
+ src/almanac_contracts.egg-info/PKG-INFO
7
+ src/almanac_contracts.egg-info/SOURCES.txt
8
+ src/almanac_contracts.egg-info/dependency_links.txt
9
+ src/almanac_contracts.egg-info/requires.txt
10
+ src/almanac_contracts.egg-info/top_level.txt
11
+ src/almanac_contracts/hosted/__init__.py
12
+ src/almanac_contracts/hosted/api_keys.py
13
+ src/almanac_contracts/hosted/billing.py
14
+ src/almanac_contracts/hosted/jobs.py
15
+ src/almanac_contracts/hosted/pages.py
16
+ src/almanac_contracts/hosted/sources.py
17
+ src/almanac_contracts/hosted/status.py
18
+ src/almanac_contracts/hosted/topics.py
19
+ src/almanac_contracts/hosted/wikis.py
20
+ src/almanac_contracts/hosted/workspaces.py
@@ -0,0 +1 @@
1
+ almanac_contracts