Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Contents

* `Return all resources across all pages as a list`_

* `Asynchronous Usage`_

* `Requests without a Workspace in Scope`_

* `Personal Access Token without a Workspace`_
Expand Down Expand Up @@ -428,6 +430,62 @@ Return all resources across all pages as a list

all_devices = paginator.flatten_to_list()

Asynchronous Usage
~~~~~~~~~~~~~~~~~~

Use ``AsyncSeam`` inside an event loop, e.g., with asyncio-based
frameworks such as FastAPI.
It accepts the same options and exposes the same API methods as ``Seam``,
except every API method is a coroutine that must be awaited.

Use the client as an async context manager,
or call ``await seam.close()`` when done,
to release the underlying connection pool.

.. code-block:: python

import asyncio

from seam import AsyncSeam


async def main():
async with AsyncSeam() as seam:
devices = await seam.devices.list()

lock = await seam.locks.get(name="Front Door")
await seam.locks.unlock_door(device_id=lock.device_id)


asyncio.run(main())

Requests run concurrently with the standard asyncio tools.

.. code-block:: python

async def list_resources(seam):
return await asyncio.gather(
seam.devices.list(),
seam.connected_accounts.list(),
)

Paginate with the same ``create_paginator`` helper.
The paginator methods are coroutines,
and ``flatten`` returns an async generator.

.. code-block:: python

async def list_connected_accounts(seam):
paginator = seam.create_paginator(seam.connected_accounts.list, {"limit": 20})

connected_accounts, pagination = await paginator.first_page()

async for account in paginator.flatten():
print(account.account_type_display_name)

The ``AsyncSeamWithoutWorkspace`` client is the equivalent async variant of
``SeamWithoutWorkspace``.

Requests without a Workspace in Scope
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
2 changes: 1 addition & 1 deletion codegen/layouts/partials/abstract-route-class.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class {{className}}(abc.ABC):
{{#each methods}}

@abc.abstractmethod
def {{> method-signature}}:
{{#if ../isAsync}}async {{/if}}def {{> method-signature}}:
"""{{> method-docstring}}"""
raise NotImplementedError()
{{/each}}
4 changes: 2 additions & 2 deletions codegen/layouts/partials/abstract-routes.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@dataclass
class AbstractRoutes(abc.ABC):
{{#each routesNamespaces}}
class {{className}}(abc.ABC):
{{#each namespaces}}
{{namespace}}: {{abstractClassName}}
{{/each}}
6 changes: 3 additions & 3 deletions codegen/layouts/partials/route-method.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@route_metadata(path="{{path}}", has_required_parameters={{#if hasRequiredParameters}}True{{else}}False{{/if}}, has_pagination={{#if hasPagination}}True{{else}}False{{/if}})
def {{> method-signature}}:
{{#if isAsync}}async {{/if}}def {{> method-signature}}:
"""{{> method-docstring}}"""
{{payloadVar}}: Dict[str, Any] = {}

Expand All @@ -13,7 +13,7 @@
raise ValueError("At least one parameter is required for {{path}}")
{{/if}}

{{#unless (eq returnType "None")}}res = {{/unless}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}})
{{#unless (eq returnType "None")}}res = {{/unless}}{{#if isAsync}}await {{/if}}self.client.{{httpVerb}}("{{path}}", {{payloadArg}}={{payloadVar}})
{{#if (eq returnType "ActionAttempt")}}

wait_for_action_attempt = (
Expand All @@ -22,7 +22,7 @@
else wait_for_action_attempt
)

return resolve_action_attempt(
return {{#if isAsync}}await resolve_action_attempt_async{{else}}resolve_action_attempt{{/if}}(
client=self.client,
action_attempt=ActionAttempt.from_dict(res["action_attempt"]),
wait_for_action_attempt=wait_for_action_attempt
Expand Down
32 changes: 29 additions & 3 deletions codegen/layouts/route.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Optional, Any, List, Dict, Literal, Union
import abc
from ..client import SeamHttpClient
from ..client import SeamHttpClient, AsyncSeamHttpClient
from ..route import route_metadata
{{#if importNull}}
from ..null import Null
Expand All @@ -9,16 +9,19 @@ from ..null import Null
from ..resources import ({{#each resourceClasses}}{{this}}{{#unless @last}},{{/unless}}{{/each}})
{{/if}}
{{#each childClasses}}
from .{{module}} import {{abstractClassName}}, {{className}}
from .{{module}} import {{abstractClassName}}, {{className}}, {{asyncAbstractClassName}}, {{asyncClassName}}
{{/each}}
{{#if importResolveActionAttempt}}
from ..modules.action_attempts import resolve_action_attempt
from ..modules.action_attempts import resolve_action_attempt, resolve_action_attempt_async
{{/if}}


{{> abstract-route-class abstractClass}}


{{> abstract-route-class asyncAbstractClass}}


class {{className}}({{abstractClassName}}):
{{#if isDeprecated}}
""".. deprecated::
Expand All @@ -40,3 +43,26 @@ class {{className}}({{abstractClassName}}):

{{> route-method}}
{{/each}}


class {{asyncClassName}}({{asyncAbstractClassName}}):
{{#if isDeprecated}}
""".. deprecated::
This route is deprecated."""
{{/if}}
def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]):
self.client = client
self.defaults = defaults
{{#each childClasses}}
self._{{namespace}} = {{asyncClassName}}(client=client, defaults=defaults)
{{/each}}
{{#each childClasses}}

@property
def {{namespace}}(self) -> {{asyncClassName}}:
return self._{{namespace}}
{{/each}}
{{#each methods}}

{{> route-method isAsync=true}}
{{/each}}
16 changes: 13 additions & 3 deletions codegen/layouts/routes-index.hbs
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
from typing import Any, Dict
import abc
from dataclasses import dataclass
from ..client import SeamHttpClient
from ..client import SeamHttpClient, AsyncSeamHttpClient
{{#each namespaces}}
from .{{namespace}} import {{abstractClassName}}, {{className}}
from .{{namespace}} import {{abstractClassName}}, {{className}}, {{asyncAbstractClassName}}, {{asyncClassName}}
{{/each}}


{{> abstract-routes}}
{{> abstract-routes abstractRoutes}}


{{> abstract-routes asyncAbstractRoutes}}


class Routes(AbstractRoutes):
def __init__(self, client: SeamHttpClient, defaults: Dict[str, Any]):
{{#each namespaces}}
self.{{namespace}} = {{className}}(client=client, defaults=defaults)
{{/each}}


class AsyncRoutes(AbstractAsyncRoutes):
def __init__(self, client: AsyncSeamHttpClient, defaults: Dict[str, Any]):
{{#each namespaces}}
self.{{namespace}} = {{asyncClassName}}(client=client, defaults=defaults)
{{/each}}
30 changes: 28 additions & 2 deletions codegen/lib/layouts/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface MethodLayoutContext {

export interface AbstractClassLayoutContext {
className: string
isAsync: boolean
isDeprecated: boolean
showPass: boolean
childProperties: Array<{ namespace: string; abstractClassName: string }>
Expand All @@ -44,13 +45,18 @@ export interface AbstractClassLayoutContext {
export interface RouteLayoutContext {
className: string
abstractClassName: string
asyncClassName: string
asyncAbstractClassName: string
isDeprecated: boolean
abstractClass: AbstractClassLayoutContext
asyncAbstractClass: AbstractClassLayoutContext
resourceClasses: string[]
childClasses: Array<{
namespace: string
className: string
abstractClassName: string
asyncClassName: string
asyncAbstractClassName: string
module: string
}>
importResolveActionAttempt: boolean
Expand Down Expand Up @@ -109,32 +115,52 @@ export const setRouteLayoutContext = (cls: ClassModel): RouteLayoutContext => {
)

const abstractClassName = `Abstract${cls.name}`
const asyncClassName = `Async${cls.name}`
const asyncAbstractClassName = `AbstractAsync${cls.name}`
const methods = cls.methods.map(getMethodLayoutContext)

const importNull = methods.some(({ params }) =>
params.some(({ isNullable }) => isNullable),
)

const showPass =
cls.methods.length === 0 && cls.childClassIdentifiers.length === 0

return {
className: cls.name,
abstractClassName,
asyncClassName,
asyncAbstractClassName,
isDeprecated: cls.isDeprecated,
abstractClass: {
className: abstractClassName,
isAsync: false,
isDeprecated: cls.isDeprecated,
showPass:
cls.methods.length === 0 && cls.childClassIdentifiers.length === 0,
showPass,
childProperties: cls.childClassIdentifiers.map((identifier) => ({
namespace: identifier.namespace,
abstractClassName: `Abstract${identifier.className}`,
})),
methods,
},
asyncAbstractClass: {
className: asyncAbstractClassName,
isAsync: true,
isDeprecated: cls.isDeprecated,
showPass,
childProperties: cls.childClassIdentifiers.map((identifier) => ({
namespace: identifier.namespace,
abstractClassName: `AbstractAsync${identifier.className}`,
})),
methods,
},
resourceClasses,
childClasses: cls.childClassIdentifiers.map((identifier) => ({
namespace: identifier.namespace,
className: identifier.className,
abstractClassName: `Abstract${identifier.className}`,
asyncClassName: `Async${identifier.className}`,
asyncAbstractClassName: `AbstractAsync${identifier.className}`,
module: `${cls.namespace}_${identifier.namespace}`,
})),
importResolveActionAttempt,
Expand Down
30 changes: 25 additions & 5 deletions codegen/lib/layouts/routes-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@

import { pascalCase } from 'change-case'

interface AbstractRoutesLayoutContext {
className: string
namespaces: Array<{ namespace: string; abstractClassName: string }>
}

export interface RoutesIndexLayoutContext {
namespaces: Array<{
namespace: string
className: string
abstractClassName: string
asyncClassName: string
asyncAbstractClassName: string
}>
routesNamespaces: Array<{ namespace: string; abstractClassName: string }>
abstractRoutes: AbstractRoutesLayoutContext
asyncAbstractRoutes: AbstractRoutesLayoutContext
}

export const setRoutesIndexLayoutContext = (
Expand All @@ -21,9 +29,21 @@ export const setRoutesIndexLayoutContext = (
namespace: ns,
className: pascalCase(ns),
abstractClassName: `Abstract${pascalCase(ns)}`,
asyncClassName: `Async${pascalCase(ns)}`,
asyncAbstractClassName: `AbstractAsync${pascalCase(ns)}`,
})),
routesNamespaces: topLevelNamespaces.map((ns) => ({
namespace: ns,
abstractClassName: `Abstract${pascalCase(ns)}`,
})),
abstractRoutes: {
className: 'AbstractRoutes',
namespaces: topLevelNamespaces.map((ns) => ({
namespace: ns,
abstractClassName: `Abstract${pascalCase(ns)}`,
})),
},
asyncAbstractRoutes: {
className: 'AbstractAsyncRoutes',
namespaces: topLevelNamespaces.map((ns) => ({
namespace: ns,
abstractClassName: `AbstractAsync${pascalCase(ns)}`,
})),
},
})
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dev = [
"pytest-watch>=4.2.0,<5",
"rstcheck>=6.3.0,<7",
"mypy>=2.3.0,<3",
"pytest-asyncio>=1.0.0,<2",
]

[build-system]
Expand All @@ -48,3 +49,5 @@ target-version = ["py311"]
norecursedirs = [
"node_modules"
]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
4 changes: 2 additions & 2 deletions seam/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# flake8: noqa

from .seam import Seam
from .seam_without_workspace import SeamWithoutWorkspace
from .seam import AsyncSeam, Seam
from .seam_without_workspace import AsyncSeamWithoutWorkspace, SeamWithoutWorkspace
from httpx_retries import Retry
from .options import SeamInvalidOptionsError
from .auth import SeamInvalidTokenError
Expand Down
Loading
Loading