generated from coulomb/repo-seed
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:
12
hub_core/utils/__init__.py
Normal file
12
hub_core/utils/__init__.py
Normal 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",
|
||||
]
|
||||
22
hub_core/utils/pagination.py
Normal file
22
hub_core/utils/pagination.py
Normal 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
13
hub_core/utils/paths.py
Normal 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
27
hub_core/utils/routing.py
Normal 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
14
hub_core/utils/slugs.py
Normal 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("-")
|
||||
Reference in New Issue
Block a user