Skip to content

Commit fee0ade

Browse files
committed
Use pydantic
1 parent 3178372 commit fee0ade

11 files changed

Lines changed: 453 additions & 333 deletions

File tree

metablock/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from .client import Metablock
22
from .components import MetablockEntity, MetablockError, MetablockResponseError
3-
from .extensions import Extension, Plugin
3+
from .extensions import Extension
44
from .orgs import Org
5-
from .spaces import Block, Service, Space, SpaceExtension
5+
from .spaces import Block, Space, SpaceExtension
66
from .user import User
77

88
__version__ = "1.1.1"
@@ -13,10 +13,8 @@
1313
"MetablockResponseError",
1414
"MetablockEntity",
1515
"Space",
16-
"Service",
1716
"Block",
1817
"Extension",
19-
"Plugin",
2018
"SpaceExtension",
2119
"Org",
2220
"User",

metablock/cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,14 @@ async def _apply(path: str, space_name: str, token: str, dry_run: bool) -> None:
125125
raise click.Abort()
126126
async with Metablock(auth_key=token) as mb:
127127
space = await mb.spaces.get(space_name)
128-
svc = await space.blocks.get_list()
129-
click.echo(f"space {space.name} has {len(svc)} blocks")
130-
by_name = {s["name"]: s for s in svc}
128+
blocks = await space.blocks.get_list()
129+
click.echo(f"space {space.name} has {len(blocks)} blocks")
130+
by_name = {s.name: s for s in blocks}
131131
for name, config in blocks:
132132
block = by_name.get(name)
133133
if block:
134134
# update
135-
await mb.blocks.update(block.id, **config)
135+
await mb.blocks.patch(block.id, **config)
136136
click.echo(f"updated block {name}")
137137
else:
138138
# create

metablock/client.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from httpx import Response as ClientResponse
1010

1111
from .components import Callback, HttpComponent, MetablockResponseError
12-
from .extensions import Extension, Extensions, Plugin, Plugins
13-
from .orgs import Org, Orgs
14-
from .spaces import Block, Blocks, Domains, Space, Spaces
12+
from .extensions import Extensions
13+
from .orgs import Orgs
14+
from .spaces import Blocks, Domains, Spaces
1515
from .user import User
1616

1717
DEFAULT_USER_AGENT = f"Python/{'.'.join(map(str, sys.version_info[:2]))} metablock"
@@ -42,12 +42,11 @@ def __init__(
4242
"user-agent": user_agent,
4343
"accept": "application/json",
4444
}
45-
self.orgs: Orgs = Orgs(self, Org)
46-
self.spaces: Spaces = Spaces(self, Space)
47-
self.blocks: Blocks = Blocks(self, Block)
48-
self.plugins: Plugins = Plugins(self, Plugin)
49-
self.extensions: Extensions = Extensions(self, Extension)
50-
self.domains = Domains(self)
45+
self.orgs: Orgs = Orgs(root=self, root_path="orgs")
46+
self.spaces: Spaces = Spaces(root=self, root_path="spaces")
47+
self.blocks: Blocks = Blocks(root=self, root_path="blocks")
48+
self.extensions: Extensions = Extensions(root=self, root_path="extensions")
49+
self.domains = Domains(root=self, root_path="domains")
5150

5251
@property
5352
def cli(self) -> Self:
@@ -65,25 +64,31 @@ async def __aexit__(self, exc_type: type, exc_val: Any, exc_tb: Any) -> None:
6564
await self.close()
6665

6766
async def spec(self) -> dict:
67+
"""Get the OpenAPI specification of the API"""
6868
return await self.request(f"{self.url}/openapi.json")
6969

7070
async def get(self, url: str, **kwargs: Any) -> Any:
71+
"""Make a GET request to the API"""
7172
kwargs["method"] = "GET"
7273
return await self.request(url, **kwargs)
7374

7475
async def patch(self, url: str, **kwargs: Any) -> Any:
76+
"""Make a PATCH request to the API"""
7577
kwargs["method"] = "PATCH"
7678
return await self.request(url, **kwargs)
7779

7880
async def post(self, url: str, **kwargs: Any) -> Any:
81+
"""Make a POST request to the API"""
7982
kwargs["method"] = "POST"
8083
return await self.request(url, **kwargs)
8184

8285
async def put(self, url: str, **kwargs: Any) -> Any:
86+
"""Make a PUT request to the API"""
8387
kwargs["method"] = "PUT"
8488
return await self.request(url, **kwargs)
8589

8690
async def delete(self, url: str, **kwargs: Any) -> Any:
91+
"""Make a DELETE request to the API"""
8792
kwargs["method"] = "DELETE"
8893
return await self.request(url, **kwargs)
8994

@@ -96,6 +101,7 @@ async def request(
96101
wrap: Any = None,
97102
**kw: Any,
98103
) -> Any:
104+
"""Make a request to the API with the given method, url, headers and body."""
99105
if not self.session:
100106
self.session = AsyncClient()
101107
method = method or "GET"
@@ -122,22 +128,19 @@ async def handle_response(self, response: ClientResponse, wrap: Any = None) -> A
122128
data = response.json()
123129
return wrap(data) if wrap else data
124130

125-
async def get_user(self, **kw: Any) -> User:
126-
kw.setdefault("wrap", self._user)
127-
return await self.get(f"{self.url}/user", **kw)
131+
async def get_user(self) -> User:
132+
data = await self.get(f"{self.url}/user")
133+
return User(root=self, root_path="user", **data)
128134

129-
async def update_user(self, **kw: Any) -> User:
130-
kw.setdefault("wrap", self._user)
131-
return await self.patch(f"{self.url}/user", **kw)
135+
async def update_user(self, **params: Any) -> User:
136+
data = await self.patch(f"{self.url}/user", **params)
137+
return User(root=self, root_path="user", **data)
132138

133-
async def delete_user(self, **kw: Any) -> None:
134-
return await self.delete(f"{self.url}/user", **kw)
139+
async def delete_user(self) -> None:
140+
return await self.delete(f"{self.url}/user")
135141

136142
def get_default_headers(self) -> dict[str, str]:
137143
headers = self.default_headers.copy()
138144
if self.auth_key:
139145
headers[self.auth_key_name] = self.auth_key
140146
return headers
141-
142-
def _user(self, data: dict) -> User:
143-
return User(self, data)

metablock/components.py

Lines changed: 12 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
from __future__ import annotations
22

33
import json
4-
from abc import ABC, abstractmethod
5-
from typing import (
6-
TYPE_CHECKING,
7-
Any,
8-
AsyncIterator,
9-
Awaitable,
10-
Callable,
11-
Generic,
12-
Mapping,
13-
TypeVar,
14-
cast,
15-
)
4+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Protocol, runtime_checkable
165

176
from httpx import Response as ClientResponse
7+
from pydantic import BaseModel, ConfigDict, Field
188

19-
from .utils import as_dict, as_params
9+
from .utils import as_dict
2010

2111
if TYPE_CHECKING: # pragma: no cover
2212
from .client import Metablock
@@ -45,212 +35,33 @@ def __str__(self) -> str:
4535
return json.dumps(self.message, indent=4)
4636

4737

48-
class HttpComponent(ABC):
38+
@runtime_checkable
39+
class HttpComponent(Protocol):
4940
@property
50-
@abstractmethod
5141
def cli(self) -> Metablock: # pragma: no cover
5242
...
5343

5444
@property
55-
@abstractmethod
5645
def url(self) -> str: # pragma: no cover
5746
...
5847

59-
def __repr__(self) -> str:
60-
return self.url
61-
62-
def __str__(self) -> str:
63-
return self.__repr__()
6448

49+
class MetablockComponent(BaseModel):
50+
model_config = ConfigDict(arbitrary_types_allowed=True)
6551

66-
class Component(HttpComponent):
67-
def __init__(
68-
self, root: Metablock | CrudComponent | MetablockEntity, name: str = ""
69-
) -> None:
70-
self.root = root
71-
self.name = name or self.__class__.__name__.lower()
52+
root: HttpComponent = Field(exclude=True)
53+
root_path: str = Field(exclude=True)
7254

7355
@property
7456
def cli(self) -> Metablock:
7557
return self.root.cli
7658

7759
@property
7860
def url(self) -> str:
79-
return f"{self.cli.url}/{self.name}"
80-
81-
@property
82-
def parent_url(self) -> str:
83-
return f"{self.root.url}/{self.name}"
84-
85-
@property
86-
def is_entity(self) -> bool:
87-
return isinstance(self.root, MetablockEntity)
61+
return f"{self.root.url}/{self.root_path}"
8862

8963

90-
class MetablockEntity(HttpComponent):
64+
class MetablockEntity(MetablockComponent):
9165
"""A Metablock entity"""
9266

93-
__slots__ = ("root", "data")
94-
95-
def __init__(
96-
self,
97-
root: Metablock | CrudComponent | MetablockEntity,
98-
data: dict,
99-
) -> None:
100-
self.root = root
101-
self.data = data
102-
103-
def __repr__(self) -> str:
104-
return repr(self.data)
105-
106-
def __getitem__(self, item: str) -> Any:
107-
return self.data[item]
108-
109-
def __contains__(self, item: str) -> bool:
110-
return item in self.data
111-
112-
def __eq__(self, other: Any) -> bool:
113-
return isinstance(other, self.__class__) and self.data == other.data
114-
115-
def _asdict(self) -> dict:
116-
return self.data
117-
118-
@property
119-
def cli(self) -> Metablock:
120-
return self.root.cli
121-
122-
@property
123-
def id(self) -> str:
124-
return self.data.get("id", "")
125-
126-
@property
127-
def name(self) -> str:
128-
return self.data.get("name", "")
129-
130-
@property
131-
def url(self) -> str:
132-
return "%s/%s" % (self.root.url, self.id)
133-
134-
def nice(self) -> str:
135-
return json.dumps(self.data, indent=4)
136-
137-
138-
E = TypeVar("E", bound="MetablockEntity")
139-
140-
141-
class CrudComponent(Component, Generic[E]):
142-
def __init__(
143-
self,
144-
root: Metablock | CrudComponent | MetablockEntity,
145-
factory: type[E],
146-
name: str = "",
147-
) -> None:
148-
super().__init__(root, name)
149-
self.factory = factory
150-
151-
async def paginate(self, **params: Any) -> AsyncIterator[E]:
152-
next_ = self.list_create_url()
153-
url_params: Any = as_params(**params)
154-
while next_:
155-
next_, data = await self.cli.request(
156-
next_, params=url_params, callback=self._paginated
157-
)
158-
url_params = None
159-
for d in data:
160-
yield self.entity(d)
161-
162-
async def get_list(self, **kwargs: Any) -> list[E]:
163-
url = self.list_create_url()
164-
kwargs.setdefault("wrap", self.entity_list)
165-
return cast(
166-
list[E],
167-
await self.cli.request(url, **kwargs),
168-
)
169-
170-
async def get_full_list(self, **kwargs: Any) -> list[E]:
171-
return [d async for d in self.paginate(**kwargs)]
172-
173-
async def get(self, id_: str, **kwargs: Any) -> E:
174-
url = f"{self.url}/{id_}"
175-
kwargs.setdefault("wrap", self.entity)
176-
return cast(E, await self.cli.get(url, **kwargs))
177-
178-
async def has(self, id_: str, **kwargs: Any) -> bool:
179-
url = f"{self.url}/{id_}"
180-
return cast(bool, await self.cli.get(url, callback=self._head))
181-
182-
async def create(self, callback: Callback | None = None, **params: Any) -> E:
183-
url = self.list_create_url()
184-
return cast(
185-
E,
186-
await self.cli.post(url, json=params, callback=callback, wrap=self.entity),
187-
)
188-
189-
async def update(
190-
self, id_name: str, callback: Callback | None = None, **params: Any
191-
) -> E:
192-
return cast(
193-
E,
194-
await self.cli.patch(
195-
self.update_url(id_name),
196-
json=params,
197-
callback=callback,
198-
wrap=self.entity,
199-
),
200-
)
201-
202-
async def upsert(self, callback: Callback | None = None, **params: Any) -> E:
203-
return cast(
204-
E,
205-
await self.cli.put(
206-
self.url, json=params, callback=callback, wrap=self.entity
207-
),
208-
)
209-
210-
async def delete( # type: ignore
211-
self,
212-
id_name: str,
213-
**kwargs: Any,
214-
) -> Any:
215-
return await self.cli.delete(self.delete_url(id_name), **kwargs)
216-
217-
async def delete_all(self) -> int:
218-
n = 0
219-
async for entity in self.paginate():
220-
await self.delete(entity.id)
221-
n += 1
222-
return n
223-
224-
def entity(self, data: dict) -> E:
225-
return self.factory(self, data)
226-
227-
def entity_list(self, data: list[dict]) -> list[E]:
228-
return [self.entity(d) for d in data]
229-
230-
def list_create_url(self) -> str:
231-
return self.parent_url if self.is_entity else self.url
232-
233-
def update_url(self, id_name: str) -> str:
234-
return f"{self.url}/{id_name}"
235-
236-
def delete_url(self, id_name: str) -> str:
237-
return f"{self.url}/{id_name}"
238-
239-
# callbacks
240-
241-
async def _head(self, response: ClientResponse) -> bool:
242-
if response.status_code == 404:
243-
return False
244-
elif response.status_code == 200:
245-
return True
246-
else: # pragma: no cover
247-
raise MetablockResponseError(response)
248-
249-
async def _paginated(self, response: ClientResponse) -> Any:
250-
next_ = response.links.get("next")
251-
if isinstance(next_, Mapping):
252-
url = next_.get("url")
253-
else:
254-
url = None
255-
data = await self.cli.handle_response(response)
256-
return (url, data)
67+
id: str = Field(description="The unique identifier of the entity")

0 commit comments

Comments
 (0)