Add hub-core package, docs, and State Hub integration scaffold

Extract the first reusable slice (models, schemas, routers, MCP, migrations)
from state-hub with INTENT/SCOPE, agent instructions, workplan, and aligned
inter_hub capability registry index.
This commit is contained in:
2026-06-16 02:39:36 +02:00
parent d3ee203a3a
commit 986ac4d40b
52 changed files with 4085 additions and 3 deletions

View File

@@ -0,0 +1,12 @@
from hub_core.utils.pagination import PageParams, apply_pagination
from hub_core.utils.paths import resolve_repo_path
from hub_core.utils.routing import normalize_trailing_slash
from hub_core.utils.slugs import slugify
__all__ = [
"PageParams",
"apply_pagination",
"normalize_trailing_slash",
"resolve_repo_path",
"slugify",
]

View File

@@ -0,0 +1,22 @@
from dataclasses import dataclass
from typing import TypeVar
from sqlalchemy.sql import Select
SelectT = TypeVar("SelectT", bound=Select)
@dataclass(frozen=True)
class PageParams:
limit: int = 100
offset: int = 0
def __post_init__(self) -> None:
if self.limit < 1 or self.limit > 1000:
raise ValueError("limit must be between 1 and 1000")
if self.offset < 0:
raise ValueError("offset must be >= 0")
def apply_pagination(query: SelectT, page: PageParams) -> SelectT:
return query.offset(page.offset).limit(page.limit)

13
hub_core/utils/paths.py Normal file
View File

@@ -0,0 +1,13 @@
import socket
from typing import Protocol
class RepoPathLike(Protocol):
local_path: str | None
host_paths: dict
def resolve_repo_path(repo: RepoPathLike, host: str | None = None) -> str | None:
selected_host = host or socket.gethostname()
host_paths = repo.host_paths or {}
return host_paths.get(selected_host) or repo.local_path

27
hub_core/utils/routing.py Normal file
View File

@@ -0,0 +1,27 @@
from urllib.parse import SplitResult, urlsplit, urlunsplit
def normalize_trailing_slash(path_or_url: str, *, trailing: bool = True) -> str:
"""Normalize the path component while preserving query and fragment."""
if not path_or_url:
return "/" if trailing else ""
parts = urlsplit(path_or_url)
path = parts.path or "/"
if trailing:
normalized_path = path if path.endswith("/") else f"{path}/"
elif path == "/":
normalized_path = "/"
else:
normalized_path = path.rstrip("/")
if parts.scheme or parts.netloc:
return urlunsplit(
SplitResult(parts.scheme, parts.netloc, normalized_path, parts.query, parts.fragment)
)
suffix = ""
if parts.query:
suffix += f"?{parts.query}"
if parts.fragment:
suffix += f"#{parts.fragment}"
return f"{normalized_path}{suffix}"

14
hub_core/utils/slugs.py Normal file
View File

@@ -0,0 +1,14 @@
import re
_NON_SLUG = re.compile(r"[^a-z0-9]+")
_DASHES = re.compile(r"-+")
def slugify(value: str, *, max_length: int = 100) -> str:
slug = _NON_SLUG.sub("-", value.strip().lower())
slug = _DASHES.sub("-", slug).strip("-")
if not slug:
raise ValueError("slug cannot be empty")
if max_length < 1:
raise ValueError("max_length must be >= 1")
return slug[:max_length].strip("-")