diff --git a/neon_client/client.py b/neon_client/client.py index 2cea171..866c495 100644 --- a/neon_client/client.py +++ b/neon_client/client.py @@ -1,8 +1,9 @@ import os -from collections.abc import Sequence + +# from collections.abc import Sequence import requests -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel from .jsonschema import ( ApiKeysListResponseItem, @@ -10,6 +11,7 @@ from .jsonschema import ( ApiKeyCreateResponse, ApiKeyRevokeResponse, ) +from .jsonschema import ProjectListItem from .jsonschema import CurrentUserInfoResponse @@ -23,17 +25,12 @@ class NeonClientException(requests.exceptions.HTTPError): pass - - -class APIKey: - """A Neon API key.""" - +class NeonResource: def __init__( self, client, obj, data_model: BaseModel, - **kwargs, ): """A Neon API key. @@ -52,7 +49,7 @@ class APIKey: """The API key object.""" if not self.__cached_obj: - self.__cached_obj = self._data_model(**self._data) + self.__cached_obj = self._data_model.model_construct(**self._data) return self.__cached_obj @@ -64,24 +61,28 @@ class APIKey: except AttributeError: return getattr(self.obj, name) + def __getitem__(self, key): + """Get an item from the API key object or the API key data.""" + + return getattr(self.obj, key, None) or self.obj[key] + def __repr__(self): """Return a string representation of the API key.""" return repr(self.obj) + +class APIKey(NeonResource): + """A Neon API key.""" + @classmethod def create(cls, client, key_name: str): """Create a new API key.""" obj = ApiKeyCreateRequest(key_name=key_name) r = client.request("POST", "api_keys", json=obj.model_dump()) - return cls( - client=client, - obj=r, - data_model=ApiKeyCreateResponse, - ) - # ApiKeyCreateResponse(**r) + return cls(client=client, obj=r, data_model=ApiKeyCreateResponse) @classmethod def list(cls, client): @@ -93,15 +94,86 @@ class APIKey: ] @classmethod - def revoke(cls, client, api_key): + def revoke_request(cls, client, api_key): """Revoke an API key.""" r = client.request("DELETE", f"api_keys/{ api_key.obj.id }") + return cls(client=client, obj=r, data_model=ApiKeyRevokeResponse) + def revoke(self): + """Revoke the API key.""" + + return self.revoke_request(self._client, self) + + +class User(NeonResource): + """A Neon user.""" + + @classmethod + def get_current_user_info(cls, client): + """Get the current user.""" + + r = client.request("GET", "users/me") + + return cls( + client=client, + obj=r, + data_model=CurrentUserInfoResponse, + ) + + +class Project(NeonResource): + @classmethod + def list( + cls, + client, + *, + cursor: int | None = None, + limit: int | None = None, + shared: bool = False, + ): + """Get a list of projects.""" + + r_path = "projects" if not shared else "projects/shared" + r_params = {"cursor": cursor, "limit": limit} + + r = client.request("GET", r_path, params=r_params) + + return [ + cls(client=client, obj=x, data_model=ProjectListItem) for x in r["projects"] + ] + + +class Branch(NeonResource): + @classmethod + def list( + cls, + client, + project_id: str, + *, + cursor: int | None = None, + limit: int | None = None, + shared: bool = False, + ): + """Get a list of projects.""" + + r_path = "/".join(["projects", project_id, "branches"]) + r_params = {"cursor": cursor, "limit": limit} + + r = client.request("GET", r_path, params=r_params) + + return [ + cls(client=client, obj=x, data_model=ProjectListItem) for x in r["branches"] + ] + + +class Operation(NeonResource): + pass + class NeonAPI: - def __init__(self, api_key, *, base_url=None): + def __init__(self, api_key: str, *, base_url=None): if not base_url: base_url = NEON_API_BASE_URL @@ -113,8 +185,9 @@ class NeonAPI: self.base_url = base_url self.user_agent = f"neon-client/{__VERSION__}" - def request(self, method, path, **kwargs): + def request(self, method: str, path: str, **kwargs): """Send an HTTP request to the specified path using the specified method.""" + # Set HTTP headers for outgoing requests. headers = kwargs.pop("headers", {}) headers["Authorization"] = f"Bearer {self._api_key}" @@ -122,14 +195,18 @@ class NeonAPI: headers["Content-Type"] = "application/json" headers["User-Agent"] = self.user_agent + # Send the request. r = self._session.request( method, self.base_url + path, headers=headers, **kwargs ) + + # Check the response status code. try: r.raise_for_status() except requests.exceptions.HTTPError: raise NeonClientException(r.text) + # Deserialize the response. return r.json() @classmethod @@ -137,3 +214,11 @@ class NeonAPI: """Create a new Neon API client from the NEON_API_KEY environment variable.""" return cls(os.environ[NEON_API_KEY_ENVIRON]) + + def me(self): + """Get the current user.""" + return User.get_current_user_info(client=self) + + def api_keys(self): + """Get a list of API keys.""" + return APIKey.list(client=self) diff --git a/neon_client/jsonschema.py b/neon_client/jsonschema.py index 8daaece..0781fa6 100644 --- a/neon_client/jsonschema.py +++ b/neon_client/jsonschema.py @@ -590,11 +590,11 @@ class Branch1(BaseModel): class BranchCreateRequestEndpointOptions(BaseModel): type: EndpointType - autoscaling_limit_min_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_min_cu: Optional[confloat()] = Field( None, description="The minimum number of Compute Units. The minimum value is `0.25`.\n See [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\n for more information.\n", ) - autoscaling_limit_max_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_max_cu: Optional[confloat()] = Field( None, description="The maximum number of Compute Units.\n See [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\n for more information.\n", ) @@ -664,7 +664,7 @@ class CurrentUserInfoResponse(BaseModel): last_name: str projects_limit: int branches_limit: int - max_autoscaling_limit: confloat(ge=0.25) + max_autoscaling_limit: confloat() compute_seconds_limit: Optional[int] = None plan: str @@ -677,11 +677,11 @@ class EndpointSettingsData(BaseModel): class DefaultEndpointSettings(BaseModel): pg_settings: Optional[dict[str, str]] = None pgbouncer_settings: Optional[dict[str, str]] = None - autoscaling_limit_min_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_min_cu: Optional[confloat()] = Field( None, description="The minimum number of Compute Units. The minimum value is `0.25`.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) - autoscaling_limit_max_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_max_cu: Optional[confloat()] = Field( None, description="The maximum number of Compute Units. See [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) @@ -865,11 +865,11 @@ class Project1(BaseModel): settings: Optional[ProjectSettingsData] = None name: Optional[str] = Field(None, description="The project name") branch: Optional[Branch] = None - autoscaling_limit_min_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_min_cu: Optional[confloat()] = Field( None, description="DEPRECATED, use default_endpoint_settings.autoscaling_limit_min_cu instead.\n\nThe minimum number of Compute Units. The minimum value is `0.25`.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) - autoscaling_limit_max_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_max_cu: Optional[confloat()] = Field( None, description="DEPRECATED, use default_endpoint_settings.autoscaling_limit_max_cu instead.\n\nThe maximum number of Compute Units. See [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) @@ -935,10 +935,10 @@ class Endpoint(BaseModel): ..., description="The ID of the branch that the compute endpoint is associated with\n", ) - autoscaling_limit_min_cu: confloat(ge=0.25) = Field( + autoscaling_limit_min_cu: confloat() = Field( ..., description="The minimum number of Compute Units\n" ) - autoscaling_limit_max_cu: confloat(ge=0.25) = Field( + autoscaling_limit_max_cu: confloat() = Field( ..., description="The maximum number of Compute Units\n" ) region_id: str = Field(..., description="The region identifier\n") @@ -995,11 +995,11 @@ class Endpoint1(BaseModel): ) type: EndpointType settings: Optional[EndpointSettingsData] = None - autoscaling_limit_min_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_min_cu: Optional[confloat()] = Field( None, description="The minimum number of Compute Units. The minimum value is `0.25`.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) - autoscaling_limit_max_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_max_cu: Optional[confloat()] = Field( None, description="The maximum number of Compute Units.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) @@ -1032,11 +1032,11 @@ class Endpoint2(BaseModel): None, description="The destination branch ID. The destination branch must not have an exsiting read-write endpoint.\n", ) - autoscaling_limit_min_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_min_cu: Optional[confloat()] = Field( None, description="The minimum number of Compute Units. The minimum value is `0.25`.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", ) - autoscaling_limit_max_cu: Optional[confloat(ge=0.25)] = Field( + autoscaling_limit_max_cu: Optional[confloat()] = Field( None, description="The maximum number of Compute Units.\nSee [Compute size and Autoscaling configuration](https://neon.tech/docs/manage/endpoints#compute-size-and-autoscaling-configuration)\nfor more information.\n", )