Skip to content

Commit 6625661

Browse files
derek-globuspre-commit-ci[bot]kurtmckee
authored
Implement Isolated Config (#27)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Kurt McKee <contactme@kurtmckee.org>
1 parent 6c79c39 commit 6625661

File tree

16 files changed

+374
-4423
lines changed

16 files changed

+374
-4423
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
/.tox/
88
poetry.lock
99
__pycache__/
10+
.globus_registered_api/
1011
.venv/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
Changed
3+
-------
4+
5+
* Reshape ``RegisteredAPIConfig`` to better align with isolated repository structures.

src/globus_registered_api/config.py

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,124 @@
66
from __future__ import annotations
77

88
import typing as t
9+
from pathlib import Path
10+
from uuid import UUID
911

10-
from globus_sdk import Scope
12+
import click
13+
import openapi_pydantic as oa
14+
from pydantic import BaseModel
15+
from pydantic import Field
1116

17+
from globus_registered_api.domain import HTTPMethod
18+
from globus_registered_api.domain import TargetSpecifier
1219

13-
class RegisteredAPIConfig(t.TypedDict, total=False):
14-
openapi_uri: str
15-
globus_auth: GlobusAuthConfig
20+
_CONFIG_PATH = Path(".globus_registered_api/config.json")
1621

1722

18-
class GlobusAuthConfig(t.TypedDict, total=False):
19-
# Every scope used by the service mapped onto a brief human-readable description.
20-
scopes: dict[Scope, ScopeConfig]
23+
class RegisteredAPIConfig(BaseModel):
24+
# Central config, supplied at repository initialization time.
25+
core: CoreConfig
2126

27+
# A list of target configurations.
28+
# A target defines maps onto a Registered API to be synchronized via publish.
29+
targets: list[TargetConfig]
2230

23-
class ScopeConfig(t.TypedDict, total=False):
24-
# A brief human-readable description of the scope.
25-
description: str | None
31+
# A list of roles, defining access control for identities and groups.
32+
# Entities within this list must be unique (w.r.t their type and id).
33+
roles: list[RoleConfig]
2634

27-
# The targets which a caller consents to when consenting to this scope.
28-
# Either in the form of:
29-
# * A list of TargetSpecifiers in the form `["POST /v2/obj", "GET /v2/obj/{id}"]`
30-
# * The wildcard character "*", indicating that this scope applies to all targets.
31-
targets: t.Sequence[str] | t.Literal["*"]
35+
def commit(self) -> None:
36+
"""
37+
Write the current config state to disk.
38+
"""
39+
_CONFIG_PATH.parent.mkdir(exist_ok=True)
40+
_CONFIG_PATH.write_text(self.model_dump_json(indent=4))
41+
42+
@classmethod
43+
def load(cls) -> RegisteredAPIConfig:
44+
"""
45+
Read the config from disk, loading it into a RegisteredAPIConfig instance.
46+
47+
:raises click.Abort: if no config file exists.
48+
:raises ValidationError: if the config data is malformed in some way.
49+
"""
50+
if not cls.exists():
51+
click.echo("Error: Missing repository config file.")
52+
click.echo("Run 'globus-registered-api init' first to create a repository.")
53+
raise click.Abort()
54+
55+
return cls.model_validate_json(_CONFIG_PATH.read_text())
56+
57+
@classmethod
58+
def exists(cls) -> bool:
59+
"""
60+
:return: True if a config file exists on disk, False otherwise.
61+
"""
62+
return _CONFIG_PATH.is_file()
63+
64+
65+
class CoreConfig(BaseModel):
66+
"""
67+
A core config entry containing top-level service information.
68+
"""
69+
70+
# The common prefix URL for all API targets.
71+
base_url: str
72+
73+
# The OpenAPI specification for this repository.
74+
# This must be either an inline OpenAPI document or a file path/URL pointing to one.
75+
specification: str | oa.OpenAPI
76+
77+
78+
class TargetConfig(BaseModel):
79+
"""
80+
A configuration entry for a single target within a Registered API service.
81+
"""
82+
83+
# A relative API path string (e.g., /resource/{id}/action).
84+
# This will be appended to the core.base_url to form the full target URL.
85+
path: str
86+
87+
# The HTTP method for this target.
88+
method: HTTPMethod
89+
90+
# A persistent human-readable name for the target.
91+
# E.g., create-resource
92+
alias: str
93+
94+
# The list of globus-auth scope strings which independently consent to this target.
95+
scope_strings: list[str] = Field(default_factory=list)
96+
97+
@property
98+
def sort_key(self) -> tuple[str, ...]:
99+
# Sort by path then method, disambiguating duplicate targets by alias.
100+
return self.path, self.method, self.alias
101+
102+
@property
103+
def specifier(self) -> TargetSpecifier:
104+
return TargetSpecifier.create(self.method, self.path)
105+
106+
def __str__(self) -> str:
107+
return f"{self.alias} ({self.method} {self.path})"
108+
109+
110+
class RoleConfig(BaseModel):
111+
"""
112+
A configuration entry for a single identity or group.
113+
"""
114+
115+
# The type of entity this role identifies.
116+
# 'identity' refers to an entity as recognized by Globus Auth service.
117+
# 'group' refers to a group managed in the Globus Groups service.
118+
type: t.Literal["identity", "group"]
119+
120+
# The UUID of the identity or group.
121+
id: UUID
122+
123+
# The degree of permission granted to this entity.
124+
access_level: t.Literal["owner", "admin", "viewer"]
125+
126+
@property
127+
def sort_key(self) -> tuple[str, ...]:
128+
# Sort by type, then id for consistent ordering.
129+
return self.type, str(self.id)

src/globus_registered_api/domain.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,8 @@ def load(cls, value: str) -> TargetSpecifier:
106106
path=match.group("path"),
107107
content_type=match.group("content_type") or "*",
108108
)
109+
110+
def __str__(self) -> str:
111+
if self.content_type == "*":
112+
return f"{self.method} {self.path}"
113+
return f"{self.method} {self.path} {self.content_type}"

src/globus_registered_api/openapi/enchricher/security_scheme.py

Lines changed: 0 additions & 190 deletions
This file was deleted.

src/globus_registered_api/openapi/enchricher/__init__.py renamed to src/globus_registered_api/openapi/enricher/__init__.py

File renamed without changes.

src/globus_registered_api/openapi/enchricher/encricher.py renamed to src/globus_registered_api/openapi/enricher/encricher.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from globus_registered_api.config import RegisteredAPIConfig
99

1010
from .interface import SchemaMutation
11-
from .security_scheme import InjectDefaultSecuritySchemas
11+
from .security_scheme import InjectSecuritySchemes
1212

1313

1414
class OpenAPIEnricher:
@@ -19,10 +19,8 @@ class OpenAPIEnricher:
1919
RegisteredAPI.
2020
"""
2121

22-
def __init__(self, config: RegisteredAPIConfig, environment: str) -> None:
23-
self.mutations: list[SchemaMutation] = [
24-
InjectDefaultSecuritySchemas(config, environment),
25-
]
22+
def __init__(self, config: RegisteredAPIConfig) -> None:
23+
self.mutations: list[SchemaMutation] = [InjectSecuritySchemes(config)]
2624

2725
def enrich(self, schema: oa.OpenAPI) -> oa.OpenAPI:
2826
"""

src/globus_registered_api/openapi/enchricher/interface.py renamed to src/globus_registered_api/openapi/enricher/interface.py

File renamed without changes.

0 commit comments

Comments
 (0)