@seamapi/nextlove-sdk-generator
Advanced tools
Comparing version 1.5.3 to 1.5.4
@@ -16,2 +16,3 @@ import axios from "axios"; | ||
import smokeTestTemplate from "./templates/smoke-test.template.js"; | ||
import mapParentToChildrenResources from "../../lib/openapi/map-parent-to-children-resource.js"; | ||
export const generatePhpSDK = async () => { | ||
@@ -55,25 +56,10 @@ const openapi = await axios | ||
} | ||
const parent_resource_to_children_map = routes.reduce((acc, route) => { | ||
if (!route.post?.["x-fern-sdk-group-name"]) | ||
return acc; | ||
const [parent_resource_name, child_resource_name] = route.post["x-fern-sdk-group-name"]; | ||
// Making TS happy | ||
if (!parent_resource_name) | ||
return acc; | ||
if (!acc[parent_resource_name]) { | ||
acc[parent_resource_name] = []; | ||
} | ||
if (child_resource_name && | ||
!acc[parent_resource_name].includes(child_resource_name)) { | ||
acc[parent_resource_name].push(child_resource_name); | ||
} | ||
return acc; | ||
}, {}); | ||
const parent_to_children_resources_map = mapParentToChildrenResources(routes); | ||
const clients = {}; | ||
const processClient = (resource_name) => { | ||
const child_client_identifiers = (parent_resource_to_children_map[resource_name] ?? []).map((child_resource) => ({ | ||
const child_client_identifiers = (parent_to_children_resources_map[resource_name] ?? []).map((child_resource) => ({ | ||
client_name: pascalCase(`${resource_name} ${child_resource}`), | ||
namespace: child_resource, | ||
})); | ||
const is_parent_client = Object.keys(parent_resource_to_children_map).includes(resource_name); | ||
const is_parent_client = Object.keys(parent_to_children_resources_map).includes(resource_name); | ||
const pascal_resource_name = pascalCase(resource_name); | ||
@@ -80,0 +66,0 @@ clients[pascal_resource_name] = new PhpClient(pascal_resource_name, resource_name, is_parent_client, child_client_identifiers); |
@@ -17,3 +17,3 @@ export type PhpClientMethodParameter = { | ||
type Namespace = string; | ||
export type PhpClientIdentifiers = { | ||
export type PhpClientIdentifier = { | ||
client_name: ClientName; | ||
@@ -26,5 +26,5 @@ namespace: Namespace; | ||
is_parent_client: boolean; | ||
child_client_identifiers: PhpClientIdentifiers[]; | ||
child_client_identifiers: PhpClientIdentifier[]; | ||
methods: PhpClientMethod[]; | ||
constructor(client_name: ClientName, namespace: Namespace, is_parent_client: boolean, child_client_identifiers: PhpClientIdentifiers[]); | ||
constructor(client_name: ClientName, namespace: Namespace, is_parent_client: boolean, child_client_identifiers: PhpClientIdentifier[]); | ||
addMethod(method: PhpClientMethod): void; | ||
@@ -31,0 +31,0 @@ serialize(): string; |
@@ -1,2 +0,2 @@ | ||
export type ClassFileMethod = { | ||
export type ClassFileMethodBase = { | ||
method_name: string; | ||
@@ -8,5 +8,16 @@ path: string; | ||
position?: number | undefined; | ||
required?: boolean | undefined; | ||
default_value?: string; | ||
}>; | ||
}; | ||
export type ClassFileMethod = ClassFileMethodBase & ({ | ||
return_path: string[]; | ||
return_resource: string; | ||
} | { | ||
return_path: string[]; | ||
return_resource: "void"; | ||
}); | ||
type ChildClassIdentifier = { | ||
class_name: string; | ||
namespace: string; | ||
}; | ||
@@ -17,3 +28,4 @@ export declare class ClassFile { | ||
methods: ClassFileMethod[]; | ||
constructor(name: string, namespace?: string | undefined); | ||
child_class_identifiers: ChildClassIdentifier[]; | ||
constructor(name: string, namespace: string | undefined, child_class_identifiers: ChildClassIdentifier[]); | ||
addMethod(method: ClassFileMethod): void; | ||
@@ -23,1 +35,2 @@ serializeToAbstractClassWithoutImports(): string; | ||
} | ||
export {}; |
export class ClassFile { | ||
constructor(name, namespace) { | ||
constructor(name, namespace, child_class_identifiers) { | ||
this.name = name; | ||
this.namespace = namespace; | ||
this.methods = []; | ||
this.child_class_identifiers = child_class_identifiers; | ||
} | ||
@@ -11,6 +12,37 @@ addMethod(method) { | ||
serializeToAbstractClassWithoutImports() { | ||
const has_child_classes = this.child_class_identifiers.length > 0; | ||
return [ | ||
`class Abstract${this.name}(abc.ABC):`, | ||
this.methods.length === 0 ? ` pass` : "", | ||
...this.methods.map(({ method_name, parameters }) => [ | ||
`${has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => [ | ||
` @property`, | ||
` @abc.abstractmethod`, | ||
` def ${i.namespace}(self) -> Abstract${i.class_name}:`, | ||
` raise NotImplementedError()`, | ||
].join("\n")) | ||
.join("\n\n") | ||
: ""}`, | ||
...this.methods | ||
.concat(this.name === "ActionAttempts" | ||
? [ | ||
{ | ||
method_name: "poll_until_ready", | ||
parameters: [ | ||
{ name: "action_attempt_id", type: "str", required: true }, | ||
{ name: "timeout", type: "float", default_value: "5.0" }, | ||
{ | ||
name: "polling_interval", | ||
type: "float", | ||
default_value: "0.5", | ||
}, | ||
], | ||
return_path: [], | ||
return_resource: "ActionAttempt", | ||
path: "", | ||
}, | ||
] | ||
: []) | ||
.map(({ method_name, parameters }) => [ | ||
"", | ||
@@ -20,6 +52,7 @@ "", | ||
`def ${method_name}(self, ${parameters | ||
.sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999)) | ||
.map(({ name, type, position }) => position !== undefined | ||
.sort((a, b) => (a.position ?? a.required ? 1000 : 9999) - | ||
(b.position ?? b.required ? 1000 : 9999)) | ||
.map(({ name, type, required, default_value }) => required | ||
? `${name}: ${type}` | ||
: `${name}: Optional[${type}] = None`) | ||
: `${name}: Optional[${type}] = ${default_value ?? "None"}`) | ||
.join(", ")}):`, | ||
@@ -33,10 +66,19 @@ ` raise NotImplementedError()`, | ||
serializeToClass() { | ||
const validClasses = [ | ||
`Abstract${this.name}`, | ||
`AbstractSeam as Seam`, | ||
...Array.from(new Set(this.methods.map((m) => m.return_resource.replace(/^List\[/, "").replace(/\]$/, "")))), | ||
].filter((classInstance) => classInstance !== ""); | ||
const has_child_classes = this.child_class_identifiers.length > 0; | ||
return [ | ||
`from seamapi.types import (${[ | ||
`Abstract${this.name}`, | ||
`AbstractSeam as Seam`, | ||
...Array.from(new Set(this.methods.map((m) => m.return_resource.replace(/^List\[/, "").replace(/\]$/, "")))), | ||
].join(",")})`, | ||
`from typing import (Optional, Any)`, | ||
"", | ||
`from seamapi.types import (${validClasses | ||
.filter((cls) => cls !== "None") | ||
.join(",")})`, | ||
`from typing import Optional, Any, List, Dict, Union`, | ||
`${has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => `from seamapi.${this.namespace}_${i.namespace} import ${i.class_name}`) | ||
.join("\n") | ||
: ""}`, | ||
`${this.name === "ActionAttempts" ? "import time\n" : ""}`, | ||
`class ${this.name}(Abstract${this.name}):`, | ||
@@ -48,6 +90,25 @@ // TODO DOCSTRING | ||
` self.seam = seam`, | ||
`${has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => ` self._${i.namespace} = ${i.class_name}(seam=seam)`) | ||
.join("\n") | ||
: ""}`, | ||
"", | ||
`${has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => [ | ||
` @property`, | ||
` def ${i.namespace}(self) -> ${i.class_name}:`, | ||
` return self._${i.namespace}`, | ||
].join("\n")) | ||
.join("\n\n") | ||
: ""}`, | ||
...this.methods.map(({ method_name, path, return_resource, return_path, parameters }) => { | ||
const is_return_resource_a_list = return_resource.startsWith("List["); | ||
const list_item_type = return_resource.slice(5, -1); | ||
let return_resource_item = return_resource; | ||
const is_return_resource_list = return_resource_item.startsWith("List["); | ||
if (is_return_resource_list) { | ||
return_resource_item = return_resource.slice(5, -1); | ||
} | ||
const does_method_use_action_attempt = return_resource === "ActionAttempt" && !is_return_resource_list; | ||
const is_none_return_type = return_resource_item === "None"; | ||
return [ | ||
@@ -57,10 +118,18 @@ "", | ||
`def ${method_name}(self, ${parameters | ||
.sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999)) | ||
.map(({ name, type, position }) => position !== undefined | ||
.sort((a, b) => (a.position ?? a.required ? 1000 : 9999) - | ||
(b.position ?? b.required ? 1000 : 9999)) | ||
.map(({ name, type, required }) => required | ||
? `${name}: ${type}` | ||
: `${name}: Optional[${type}] = None`) | ||
.join(", ")}):`, | ||
.concat(does_method_use_action_attempt | ||
? [ | ||
"wait_for_action_attempt: Union[bool, Dict[str, float]] = True", | ||
] | ||
: []) | ||
.join(", ")}) -> ${return_resource}:`, | ||
` json_payload = {}`, | ||
"", | ||
...parameters.map(({ name }) => ` if ${name} is not None:\n json_payload["${name}"] = ${name}`), | ||
` res = self.seam.make_request(`, | ||
"", | ||
` ${is_none_return_type ? "" : "res = "}self.seam.make_request(`, | ||
` "POST",`, | ||
@@ -70,5 +139,29 @@ ` "${path}",`, | ||
` )`, | ||
is_return_resource_a_list | ||
? ` return [${list_item_type}.from_dict(item) for item in res["${return_path.join('"]["')}"]]` | ||
: ` return ${return_resource}.from_dict(res["${return_path.join('"]["')}"])`, | ||
"", | ||
does_method_use_action_attempt | ||
? [ | ||
" if isinstance(wait_for_action_attempt, dict):", | ||
` updated_action_attempt = self.seam.action_attempts.poll_until_ready(`, | ||
" res['action_attempt']['action_attempt_id'],", | ||
" timeout=wait_for_action_attempt.get('timeout', None),", | ||
" polling_interval=wait_for_action_attempt.get('polling_interval', None),", | ||
" )", | ||
" elif wait_for_action_attempt is True:", | ||
` updated_action_attempt = self.seam.action_attempts.poll_until_ready(`, | ||
" res['action_attempt']['action_attempt_id']", | ||
" )", | ||
" else:", | ||
` return ${return_resource}.from_dict(res["${return_path.join('"]["')}"])`, | ||
"", | ||
" return updated_action_attempt", | ||
].join("\n") | ||
: "", | ||
"", | ||
!does_method_use_action_attempt | ||
? is_none_return_type | ||
? ` return None` | ||
: is_return_resource_list | ||
? ` return [${return_resource_item}.from_dict(item) for item in res["${return_path.join('"]["')}"]]` | ||
: ` return ${return_resource}.from_dict(res["${return_path.join('"]["')}"])` | ||
: "", | ||
] | ||
@@ -78,2 +171,28 @@ .map((s) => ` ${s}`) | ||
}), | ||
this.name === "ActionAttempts" | ||
? [ | ||
"", | ||
" def poll_until_ready(self, action_attempt_id: str, timeout: Optional[float] = 5.0, polling_interval: Optional[float] = 0.5) -> ActionAttempt:", | ||
" seam = self.seam", | ||
" time_waiting = 0.0", | ||
"", | ||
" action_attempt = seam.action_attempts.get(action_attempt_id, wait_for_action_attempt=False)", | ||
"", | ||
" while action_attempt.status == 'pending':", | ||
" time.sleep(polling_interval)", | ||
" time_waiting += polling_interval", | ||
"", | ||
" if time_waiting > timeout:", | ||
" raise Exception('Timed out waiting for action attempt to be ready')", | ||
"", | ||
" action_attempt = seam.action_attempts.get(", | ||
" action_attempt.action_attempt_id, wait_for_action_attempt=False", | ||
" )", | ||
"", | ||
" if action_attempt.status == 'failed':", | ||
" raise Exception(f'Action Attempt failed: {action_attempt.error.message}')", | ||
"", | ||
" return action_attempt", | ||
].join("\n") | ||
: "", | ||
].join("\n"); | ||
@@ -80,0 +199,0 @@ } |
@@ -16,3 +16,12 @@ import axios from "axios"; | ||
import readmeMdTemplate from "./templates/readme.md.template.js"; | ||
import initTemplate from "./templates/__init__.py.template.js"; | ||
import gitignoreTemplate from "./templates/.gitignore.template.js"; | ||
import prebuildTemplate from "./templates/prebuild.py.template.js"; | ||
import reportErrorTemplate from "./templates/utils/report_error.py.template.js"; | ||
import getSentryDsnTemplate from "./templates/utils/get_sentry_dsn.py.template.js"; | ||
import SeamApiExceptionClassTemplate from "./templates/snippets/seam-api-exception-class.template.js"; | ||
import { getParameterAndResponseSchema } from "../../lib/openapi/get-parameter-and-response-schema.js"; | ||
import mapParentToChildrenResources from "../../lib/openapi/map-parent-to-children-resource.js"; | ||
import { deepFlattenOneOfAndAllOfSchema } from "../../lib/generate-php-sdk/utils/deep-flatten-one-of-and-all-of-schema.js"; | ||
import endpoints_returning_deprecated_action_attempt from "../../lib/endpoints-returning-deprecated-action-attempt.js"; | ||
export const generatePythonSDK = async () => { | ||
@@ -27,5 +36,13 @@ const openapi = await axios | ||
const fs = {}; | ||
fs["README.md"] = readmeMdTemplate(); | ||
const class_map = {}; | ||
const namespaces = []; | ||
const parent_to_children_resources_map = mapParentToChildrenResources(routes); | ||
const processClass = (resource_name) => { | ||
const child_class_identifiers = (parent_to_children_resources_map[resource_name] ?? []).map((child_resource) => ({ | ||
class_name: pascalCase(`${resource_name} ${child_resource}`), | ||
namespace: child_resource, | ||
})); | ||
const pascal_resource_name = pascalCase(resource_name); | ||
class_map[pascal_resource_name] = new ClassFile(pascal_resource_name, resource_name, child_class_identifiers); | ||
}; | ||
for (const route of routes) { | ||
@@ -37,9 +54,18 @@ if (!route.post) | ||
const group_names = [...route.post["x-fern-sdk-group-name"]]; | ||
const [base_resource] = group_names; | ||
const namespace = group_names.join("_"); | ||
group_names.reverse(); | ||
const class_name = pascalCase(group_names.join("_")); | ||
const class_name = pascalCase(namespace); | ||
if (!class_map[class_name]) { | ||
namespaces.push(route.post["x-fern-sdk-group-name"]); | ||
class_map[class_name] = new ClassFile(class_name, namespace); | ||
processClass(namespace); | ||
} | ||
/* | ||
special case when we don't have routes for a base resource | ||
and thus a respective x-fern-sdk-group-name for ex. /noise_sensors | ||
*/ | ||
if (base_resource && !class_map[pascalCase(base_resource)]) { | ||
namespaces.push([base_resource]); | ||
processClass(base_resource); | ||
} | ||
const cls = class_map[class_name]; | ||
@@ -51,6 +77,2 @@ if (!cls) { | ||
const { parameter_schema, response_obj_type, response_arr_type } = getParameterAndResponseSchema(route); | ||
if (!response_obj_type && !response_arr_type) { | ||
console.warn(`No response object/array ref for "${route.path}", skipping`); | ||
continue; | ||
} | ||
if (!parameter_schema) { | ||
@@ -64,6 +86,9 @@ console.warn(`No parameter schema for "${route.path}", skipping`); | ||
parameters: Object.entries(parameter_schema.properties) | ||
.filter(([_, param_val]) => param_val && typeof param_val === "object" && "type" in param_val) | ||
.filter(([_, param_val]) => "type" in param_val || | ||
("oneOf" in param_val && "type" in (param_val.oneOf[0] ?? {}))) | ||
.map(([param_name, param_val]) => ({ | ||
name: param_name, | ||
type: mapPythonType(param_val.type), | ||
type: mapPythonType("type" in param_val | ||
? param_val | ||
: deepFlattenOneOfAndAllOfSchema(param_val)), | ||
position: route.post["x-fern-sdk-method-name"] === "get" && | ||
@@ -73,7 +98,10 @@ param_name === `${route.post["x-fern-sdk-return-value"]}_id` | ||
: undefined, | ||
required: parameter_schema.required?.includes(param_name), | ||
})), | ||
return_path: [route.post["x-fern-sdk-return-value"]], | ||
return_resource: response_obj_type | ||
? pascalCase(response_obj_type) | ||
: `List[${pascalCase(response_arr_type)}]`, | ||
return_resource: determineReturnResource({ | ||
route_path: route.path, | ||
response_obj_type, | ||
response_arr_type, | ||
}), | ||
}); | ||
@@ -102,7 +130,10 @@ } | ||
"", | ||
`class SeamAPIException(Exception):`, | ||
` pass`, | ||
SeamApiExceptionClassTemplate(), | ||
"", | ||
"", | ||
...Object.entries(class_map).map(([_, cls]) => cls.serializeToAbstractClassWithoutImports()), | ||
...Object.entries(class_map) | ||
.sort( | ||
// define classes without children first for parent-child referencing | ||
([, a], [, b]) => a.child_class_identifiers.length - b.child_class_identifiers.length) | ||
.map(([_, cls]) => cls.serializeToAbstractClassWithoutImports()), | ||
"", | ||
@@ -114,7 +145,12 @@ "", | ||
].join("\n"); | ||
fs["seamapi/__init__.py"] = `from seamapi.seam import Seam`; | ||
fs["README.md"] = readmeMdTemplate(); | ||
fs["pyproject.toml"] = pyprojectTomlTemplate(); | ||
fs[".gitignore"] = gitignoreTemplate(); | ||
fs["scripts/prebuild.py"] = prebuildTemplate(); | ||
fs["seamapi/__init__.py"] = initTemplate(); | ||
fs["seamapi/routes.py"] = routesPyTemplate(top_level_namespaces); | ||
fs["seamapi/seam.py"] = seamPyTemplate(); | ||
fs["seamapi/utils/deep_attr_dict.py"] = deep_attr_dictPyTemplate(); | ||
fs["pyproject.toml"] = pyprojectTomlTemplate(); | ||
fs["seamapi/utils/report_error.py"] = reportErrorTemplate(); | ||
fs["seamapi/utils/get_sentry_dsn.py"] = getSentryDsnTemplate(); | ||
fs["tests/conftest.py"] = conftestPyTemplate(); | ||
@@ -125,2 +161,14 @@ fs["tests/test_smoke.py"] = test_smokePyTemplate(); | ||
}; | ||
function determineReturnResource({ route_path, response_arr_type, response_obj_type, }) { | ||
if (endpoints_returning_deprecated_action_attempt.includes(route_path)) { | ||
return "None"; | ||
} | ||
if (response_obj_type) { | ||
return pascalCase(response_obj_type); | ||
} | ||
if (response_arr_type) { | ||
return `List[${pascalCase(response_arr_type)}]`; | ||
} | ||
return "None"; | ||
} | ||
//# sourceMappingURL=generate-python-sdk.js.map |
import type { PropertySchema } from "../../lib/types.js"; | ||
export declare const mapPythonType: (property_schema: PropertySchema) => "int" | "float" | "bool" | "Any" | "str" | "List[Any]" | "Dict[str, Any]"; | ||
export declare const mapPythonType: (property_schema: PropertySchema) => string; |
@@ -20,4 +20,8 @@ // TODO literals? | ||
return "float"; | ||
if ("type" in property_schema && property_schema.type === "array") | ||
return "List[Any]"; // TODO, make more specific | ||
if ("type" in property_schema && property_schema.type === "array") { | ||
const array_item_type = "type" in property_schema.items | ||
? mapPythonType({ type: property_schema.items.type }) | ||
: "Any"; | ||
return `List[${array_item_type}]`; | ||
} // TODO, make more specific | ||
if ("type" in property_schema && property_schema.type === "object") | ||
@@ -24,0 +28,0 @@ return "Dict[str, Any]"; // TODO, make more specific |
@@ -1,12 +0,81 @@ | ||
export default () => `import pytest | ||
import random | ||
export default () => `import random | ||
import string | ||
import pytest | ||
from seamapi import Seam | ||
from dotenv import load_dotenv | ||
from typing import Any | ||
from dataclasses import dataclass | ||
@pytest.fixture(autouse=True) | ||
def dotenv_fixture(): | ||
load_dotenv() | ||
@dataclass | ||
class SeamBackend: | ||
url: str | ||
sandbox_api_key: str | ||
# TODO this should use scope="session", but there's some issue, this would | ||
# dramatically reduce test time to switch | ||
@pytest.fixture(scope="function") | ||
def seam(): | ||
r = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) | ||
seam = Seam(api_url=f"https://{r}.fakeseamconnect.seam.vc", api_key="seam_apikey1_token") | ||
yield seam`; | ||
def seam_backend(): | ||
random_string = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) | ||
yield SeamBackend( | ||
url=f"https://{random_string}.fakeseamconnect.seam.vc", | ||
sandbox_api_key="seam_apikey1_token", | ||
) | ||
@pytest.fixture | ||
def seam(seam_backend: Any): | ||
seam = Seam(api_url=seam_backend.url, api_key=seam_backend.sandbox_api_key) | ||
# seam.make_request("POST", "/workspaces/reset_sandbox") | ||
yield seam | ||
@pytest.fixture | ||
def fake_sentry(monkeypatch): | ||
sentry_dsn = "https://key@sentry.io/123" | ||
monkeypatch.setenv("SENTRY_DSN", sentry_dsn) | ||
sentry_init_args = {} | ||
sentry_capture_exception_calls = [] | ||
sentry_add_breadcrumb_calls = [] | ||
class TestSentryClient(object): | ||
def __init__(self, *args, **kwargs): | ||
sentry_init_args.update(kwargs) | ||
def set_context(self, *args, **kwargs): | ||
pass | ||
monkeypatch.setattr("sentry_sdk.Client", TestSentryClient) | ||
class TestSentryScope(object): | ||
def set_context(self, *args, **kwargs): | ||
pass | ||
class TestSentryHub(object): | ||
def __init__(self, *args, **kwargs): | ||
self.scope = TestSentryScope() | ||
def capture_exception(self, *args, **kwargs): | ||
sentry_capture_exception_calls.append((args, kwargs)) | ||
def add_breadcrumb(self, *args, **kwargs): | ||
sentry_add_breadcrumb_calls.append((args, kwargs)) | ||
monkeypatch.setattr("sentry_sdk.Hub", TestSentryHub) | ||
yield { | ||
"sentry_init_args": sentry_init_args, | ||
"sentry_capture_exception_calls": sentry_capture_exception_calls, | ||
"sentry_add_breadcrumb_calls": sentry_add_breadcrumb_calls, | ||
"sentry_dsn": sentry_dsn, | ||
} | ||
`; | ||
//# sourceMappingURL=conftest.py.template.js.map |
export default () => `[tool.poetry] | ||
name = "seamapi" | ||
version = "2.14.0" | ||
version = "2.18.0" | ||
description = "A Python Library for Seam's API https://getseam.com" | ||
@@ -11,2 +11,4 @@ authors = ["Severin Ibarluzea <seveibar@gmail.com>"] | ||
requests = "^2.26.0" | ||
dataclasses-json = "^0.5.6" | ||
sentry-sdk = "^1.9.10" | ||
@@ -17,2 +19,3 @@ [tool.poetry.dev-dependencies] | ||
black = "^21.12b0" | ||
testcontainers = {extras = ["postgresql"], version = "^3.4.2"} | ||
responses = "^0.22.0" | ||
@@ -22,3 +25,4 @@ | ||
requires = ["poetry-core>=1.0.0"] | ||
build-backend = "poetry.core.masonry.api"`; | ||
build-backend = "poetry.core.masonry.api" | ||
`; | ||
//# sourceMappingURL=pyproject.toml.template.js.map |
export default () => `import os | ||
from seamapi.utils.get_sentry_dsn import get_sentry_dsn | ||
from .routes import Routes | ||
import requests | ||
from importlib.metadata import version | ||
import sentry_sdk | ||
import pkg_resources | ||
from typing import Optional, cast | ||
from .types import AbstractSeam, SeamAPIException | ||
from .types import AbstractSeam, SeamApiException | ||
class Seam(AbstractSeam): | ||
""" | ||
Initial Seam class used to interact with Seam API | ||
... | ||
Attributes | ||
---------- | ||
api_key : str | ||
API key (default None) | ||
api_url : str | ||
API url (default None) | ||
workspaces : Workspaces | ||
Workspaces class | ||
connected_accounts : ConnectedAccounts | ||
Connected accounts class | ||
connect_webviews : ConnectWebviews | ||
Connect webviews class | ||
devices : Devices | ||
Devices class | ||
events : Events | ||
Events class | ||
locks : Locks | ||
Locks class | ||
access_codes : AccessCodes | ||
Access codes class | ||
action_attempts : ActionAttempts | ||
Action attempts class | ||
""" | ||
api_key: str | ||
@@ -17,2 +48,3 @@ api_url: str = "https://connect.getseam.com" | ||
api_key: Optional[str] = None, | ||
workspace_id: Optional[str] = None, | ||
api_url: Optional[str] = None, | ||
@@ -26,4 +58,8 @@ should_report_exceptions: Optional[bool] = False, | ||
API key | ||
workspace_id : str, optional | ||
Workspace id | ||
api_url : str, optional | ||
API url | ||
should_report_exceptions : bool, optional | ||
Defaults to False. If true, thrown exceptions will be reported to Seam. | ||
""" | ||
@@ -38,7 +74,31 @@ Routes.__init__(self) | ||
) | ||
if api_url is None: | ||
api_url = os.environ.get("SEAM_API_URL", self.api_url) | ||
if workspace_id is None: | ||
workspace_id = os.environ.get("SEAM_WORKSPACE_ID", None) | ||
self.api_key = api_key | ||
self.api_url = cast(str, api_url) | ||
self.workspace_id = workspace_id | ||
if os.environ.get("SEAM_API_URL", None) is not None: | ||
print( | ||
'\\n' | ||
'\\033[93m' | ||
'Using the SEAM_API_URL environment variable is deprecated. ' | ||
'Support will be removed in a later major version. Use SEAM_ENDPOINT instead.' | ||
'\\033[0m' | ||
) | ||
api_url = os.environ.get("SEAM_API_URL", None) or os.environ.get("SEAM_ENDPOINT", None) or api_url | ||
if api_url is not None: | ||
self.api_url = cast(str, api_url) | ||
self.should_report_exceptions = should_report_exceptions | ||
if self.should_report_exceptions: | ||
self.sentry_client = sentry_sdk.Hub(sentry_sdk.Client( | ||
dsn=get_sentry_dsn(), | ||
)) | ||
self.sentry_client.scope.set_context("sdk_info", { | ||
"repository": "https://github.com/seamapi/python", | ||
"version": pkg_resources.get_distribution("seamapi").version, | ||
"endpoint": self.api_url, | ||
}) | ||
def make_request(self, method: str, path: str, **kwargs): | ||
@@ -59,20 +119,36 @@ """ | ||
url = self.api_url + path | ||
sdk_version = pkg_resources.get_distribution("seamapi").version | ||
headers = { | ||
"Authorization": "Bearer " + self.api_key, | ||
"Content-Type": "application/json", | ||
"User-Agent": "Python SDK v" + version("seamapi") + " (https://github.com/seamapi/python)", | ||
"User-Agent": "Python SDK v" + sdk_version + " (https://github.com/seamapi/python)", | ||
"seam-sdk-name": "seamapi/python", | ||
"seam-sdk-version": sdk_version, | ||
} | ||
if self.workspace_id is not None: | ||
headers["seam-workspace"] = self.workspace_id | ||
response = requests.request(method, url, headers=headers, **kwargs) | ||
parsed_response = response.json() | ||
if self.should_report_exceptions and response.status_code: | ||
# Add breadcrumb | ||
self.sentry_client.add_breadcrumb( | ||
category="http", | ||
level="info", | ||
data={ | ||
"method": method, | ||
"url": url, | ||
"status_code": response.status_code, | ||
"request_id": response.headers.get("seam-request-id", "unknown"), | ||
}, | ||
) | ||
if response.status_code != 200: | ||
raise SeamAPIException( | ||
response.status_code, | ||
response.headers.get("seam-request-id", None), | ||
parsed_response["error"], | ||
) | ||
## TODO automatically paginate if kwargs["auto_paginate"] is True | ||
raise SeamApiException(response) | ||
return parsed_response`; | ||
if "application/json" in response.headers["content-type"]: | ||
return response.json() | ||
return response.text | ||
`; | ||
//# sourceMappingURL=seam.py.template.js.map |
export default () => `@dataclass | ||
class AbstractSeam(AbstractRoutes): | ||
api_key: str | ||
api_url: str | ||
api_key: str | ||
workspace_id: str | ||
api_url: str | ||
@abc.abstractmethod | ||
def __init__(self, api_key: Optional[str] = None): | ||
raise NotImplementedError`; | ||
@abc.abstractmethod | ||
def __init__( | ||
self, | ||
api_key: Optional[str] = None, | ||
workspace_id: Optional[str] = None, | ||
api_url: Optional[str] = None, | ||
should_report_exceptions: Optional[bool] = False, | ||
): | ||
raise NotImplementedError`; | ||
//# sourceMappingURL=abstract-seam.template.js.map |
@@ -8,4 +8,9 @@ export default (name, parameters) => `@dataclass | ||
return ${name}( | ||
${parameters.map((p) => ` ${p.name}=d.get("${p.name}", None),`).join("\n")} | ||
${parameters | ||
.map((p) => { | ||
const is_dict_param = p.type.startsWith("Dict") || p.name === "properties"; | ||
return ` ${p.name}=${is_dict_param ? "DeepAttrDict(" : ""}d.get("${p.name}", None)${is_dict_param ? ")" : ""},`; | ||
}) | ||
.join("\n")} | ||
)`; | ||
//# sourceMappingURL=resource-dataclass.template.js.map |
@@ -32,12 +32,12 @@ import { flattenObjSchema } from "./flatten-obj-schema.js"; | ||
} | ||
const response_obj_type = response_obj_ref?.split("/")?.pop(); | ||
const response_arr_type = response_arr_ref?.split("/")?.pop(); | ||
return { | ||
response_obj_type, | ||
response_arr_type, | ||
parameter_schema, | ||
res_return_schema: res_return_schema, | ||
nullable, | ||
}; | ||
else { | ||
return { | ||
response_obj_type: response_obj_ref?.split("/")?.pop(), | ||
response_arr_type: response_arr_ref?.split("/")?.pop(), | ||
parameter_schema, | ||
res_return_schema: res_return_schema, | ||
nullable, | ||
}; | ||
} | ||
}; | ||
//# sourceMappingURL=get-parameter-and-response-schema.js.map |
{ | ||
"name": "@seamapi/nextlove-sdk-generator", | ||
"version": "1.5.3", | ||
"version": "1.5.4", | ||
"description": "Utilities for building NextLove SDK Generators", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -7,3 +7,3 @@ import axios from "axios" | ||
import { getPhpType } from "./utils/get-php-type.js" | ||
import { PhpClient, type PhpClientIdentifiers } from "./utils/php-client.js" | ||
import { PhpClient, type PhpClientIdentifier } from "./utils/php-client.js" | ||
import { generateSeamClient } from "./utils/generate-seam-client.js" | ||
@@ -18,2 +18,3 @@ import { deepExtractResourceObjectSchemas } from "./utils/deep-extract-resource-object-schemas.js" | ||
import smokeTestTemplate from "./templates/smoke-test.template.js" | ||
import mapParentToChildrenResources from "lib/openapi/map-parent-to-children-resource.js" | ||
@@ -69,33 +70,8 @@ export const generatePhpSDK = async () => { | ||
const parent_resource_to_children_map = routes.reduce( | ||
(acc: Record<string, string[]>, route) => { | ||
if (!route.post?.["x-fern-sdk-group-name"]) return acc | ||
const [parent_resource_name, child_resource_name] = | ||
route.post["x-fern-sdk-group-name"] | ||
// Making TS happy | ||
if (!parent_resource_name) return acc | ||
if (!acc[parent_resource_name]) { | ||
acc[parent_resource_name] = [] | ||
} | ||
if ( | ||
child_resource_name && | ||
!acc[parent_resource_name]!.includes(child_resource_name) | ||
) { | ||
acc[parent_resource_name]!.push(child_resource_name) | ||
} | ||
return acc | ||
}, | ||
{} | ||
) | ||
const parent_to_children_resources_map = mapParentToChildrenResources(routes) | ||
const clients: Record<string, PhpClient> = {} | ||
const processClient = (resource_name: string) => { | ||
const child_client_identifiers: PhpClientIdentifiers[] = ( | ||
parent_resource_to_children_map[resource_name] ?? [] | ||
const child_client_identifiers: PhpClientIdentifier[] = ( | ||
parent_to_children_resources_map[resource_name] ?? [] | ||
).map((child_resource) => ({ | ||
@@ -106,3 +82,3 @@ client_name: pascalCase(`${resource_name} ${child_resource}`), | ||
const is_parent_client = Object.keys( | ||
parent_resource_to_children_map | ||
parent_to_children_resources_map | ||
).includes(resource_name) | ||
@@ -109,0 +85,0 @@ const pascal_resource_name = pascalCase(resource_name) |
@@ -19,3 +19,3 @@ export type PhpClientMethodParameter = { | ||
type Namespace = string | ||
export type PhpClientIdentifiers = { | ||
export type PhpClientIdentifier = { | ||
client_name: ClientName | ||
@@ -32,3 +32,3 @@ namespace: Namespace | ||
public is_parent_client: boolean, | ||
public child_client_identifiers: PhpClientIdentifiers[] | ||
public child_client_identifiers: PhpClientIdentifier[] | ||
) { | ||
@@ -35,0 +35,0 @@ this.methods = [] |
@@ -1,2 +0,2 @@ | ||
export type ClassFileMethod = { | ||
export type ClassFileMethodBase = { | ||
method_name: string | ||
@@ -8,7 +8,24 @@ path: string | ||
position?: number | undefined | ||
required?: boolean | undefined | ||
default_value?: string | ||
}> | ||
return_path: string[] | ||
return_resource: string | ||
} | ||
export type ClassFileMethod = ClassFileMethodBase & | ||
( | ||
| { | ||
return_path: string[] | ||
return_resource: string | ||
} | ||
| { | ||
return_path: string[] | ||
return_resource: "void" | ||
} | ||
) | ||
type ChildClassIdentifier = { | ||
class_name: string | ||
namespace: string | ||
} | ||
export class ClassFile { | ||
@@ -18,7 +35,13 @@ name: string | ||
methods: ClassFileMethod[] | ||
child_class_identifiers: ChildClassIdentifier[] | ||
constructor(name: string, namespace?: string | undefined) { | ||
constructor( | ||
name: string, | ||
namespace: string | undefined, | ||
child_class_identifiers: ChildClassIdentifier[] | ||
) { | ||
this.name = name | ||
this.namespace = namespace | ||
this.methods = [] | ||
this.child_class_identifiers = child_class_identifiers | ||
} | ||
@@ -31,23 +54,65 @@ | ||
serializeToAbstractClassWithoutImports(): string { | ||
const has_child_classes = this.child_class_identifiers.length > 0 | ||
return [ | ||
`class Abstract${this.name}(abc.ABC):`, | ||
this.methods.length === 0 ? ` pass` : "", | ||
...this.methods.map(({ method_name, parameters }) => | ||
[ | ||
"", | ||
"", | ||
`@abc.abstractmethod`, | ||
`def ${method_name}(self, ${parameters | ||
.sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999)) | ||
.map(({ name, type, position }) => | ||
position !== undefined | ||
? `${name}: ${type}` | ||
: `${name}: Optional[${type}] = None` | ||
) | ||
.join(", ")}):`, | ||
` raise NotImplementedError()`, | ||
] | ||
.map((s) => ` ${s}`) | ||
.join("\n") | ||
), | ||
`${ | ||
has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => | ||
[ | ||
` @property`, | ||
` @abc.abstractmethod`, | ||
` def ${i.namespace}(self) -> Abstract${i.class_name}:`, | ||
` raise NotImplementedError()`, | ||
].join("\n") | ||
) | ||
.join("\n\n") | ||
: "" | ||
}`, | ||
...this.methods | ||
.concat( | ||
this.name === "ActionAttempts" | ||
? [ | ||
{ | ||
method_name: "poll_until_ready", | ||
parameters: [ | ||
{ name: "action_attempt_id", type: "str", required: true }, | ||
{ name: "timeout", type: "float", default_value: "5.0" }, | ||
{ | ||
name: "polling_interval", | ||
type: "float", | ||
default_value: "0.5", | ||
}, | ||
], | ||
return_path: [], | ||
return_resource: "ActionAttempt", | ||
path: "", | ||
}, | ||
] | ||
: [] | ||
) | ||
.map(({ method_name, parameters }) => | ||
[ | ||
"", | ||
"", | ||
`@abc.abstractmethod`, | ||
`def ${method_name}(self, ${parameters | ||
.sort( | ||
(a, b) => | ||
(a.position ?? a.required ? 1000 : 9999) - | ||
(b.position ?? b.required ? 1000 : 9999) | ||
) | ||
.map(({ name, type, required, default_value }) => | ||
required | ||
? `${name}: ${type}` | ||
: `${name}: Optional[${type}] = ${default_value ?? "None"}` | ||
) | ||
.join(", ")}):`, | ||
` raise NotImplementedError()`, | ||
] | ||
.map((s) => ` ${s}`) | ||
.join("\n") | ||
), | ||
].join("\n") | ||
@@ -57,16 +122,32 @@ } | ||
serializeToClass(): string { | ||
const validClasses = [ | ||
`Abstract${this.name}`, | ||
`AbstractSeam as Seam`, | ||
...Array.from( | ||
new Set( | ||
this.methods.map((m) => | ||
m.return_resource.replace(/^List\[/, "").replace(/\]$/, "") | ||
) | ||
) | ||
), | ||
].filter((classInstance) => classInstance !== "") | ||
const has_child_classes = this.child_class_identifiers.length > 0 | ||
return [ | ||
`from seamapi.types import (${[ | ||
`Abstract${this.name}`, | ||
`AbstractSeam as Seam`, | ||
...Array.from( | ||
new Set( | ||
this.methods.map((m) => | ||
m.return_resource.replace(/^List\[/, "").replace(/\]$/, "") | ||
) | ||
) | ||
), | ||
].join(",")})`, | ||
`from typing import (Optional, Any)`, | ||
"", | ||
`from seamapi.types import (${validClasses | ||
.filter((cls) => cls !== "None") | ||
.join(",")})`, | ||
`from typing import Optional, Any, List, Dict, Union`, | ||
`${ | ||
has_child_classes | ||
? this.child_class_identifiers | ||
.map( | ||
(i) => | ||
`from seamapi.${this.namespace}_${i.namespace} import ${i.class_name}` | ||
) | ||
.join("\n") | ||
: "" | ||
}`, | ||
`${this.name === "ActionAttempts" ? "import time\n" : ""}`, | ||
`class ${this.name}(Abstract${this.name}):`, | ||
@@ -78,7 +159,39 @@ // TODO DOCSTRING | ||
` self.seam = seam`, | ||
`${ | ||
has_child_classes | ||
? this.child_class_identifiers | ||
.map( | ||
(i) => ` self._${i.namespace} = ${i.class_name}(seam=seam)` | ||
) | ||
.join("\n") | ||
: "" | ||
}`, | ||
"", | ||
`${ | ||
has_child_classes | ||
? this.child_class_identifiers | ||
.map((i) => | ||
[ | ||
` @property`, | ||
` def ${i.namespace}(self) -> ${i.class_name}:`, | ||
` return self._${i.namespace}`, | ||
].join("\n") | ||
) | ||
.join("\n\n") | ||
: "" | ||
}`, | ||
...this.methods.map( | ||
({ method_name, path, return_resource, return_path, parameters }) => { | ||
const is_return_resource_a_list = return_resource.startsWith("List[") | ||
const list_item_type = return_resource.slice(5, -1) | ||
let return_resource_item = return_resource | ||
const is_return_resource_list = | ||
return_resource_item.startsWith("List[") | ||
if (is_return_resource_list) { | ||
return_resource_item = return_resource.slice(5, -1) | ||
} | ||
const does_method_use_action_attempt = | ||
return_resource === "ActionAttempt" && !is_return_resource_list | ||
const is_none_return_type = return_resource_item === "None" | ||
return [ | ||
@@ -88,10 +201,24 @@ "", | ||
`def ${method_name}(self, ${parameters | ||
.sort((a, b) => (a.position ?? 9999) - (b.position ?? 9999)) | ||
.map(({ name, type, position }) => | ||
position !== undefined | ||
.sort( | ||
(a, b) => | ||
(a.position ?? a.required ? 1000 : 9999) - | ||
(b.position ?? b.required ? 1000 : 9999) | ||
) | ||
.map(({ name, type, required }) => | ||
required | ||
? `${name}: ${type}` | ||
: `${name}: Optional[${type}] = None` | ||
) | ||
.join(", ")}):`, | ||
.concat( | ||
does_method_use_action_attempt | ||
? [ | ||
"wait_for_action_attempt: Union[bool, Dict[str, float]] = True", | ||
] | ||
: [] | ||
) | ||
.join(", ")}) -> ${return_resource}:`, | ||
` json_payload = {}`, | ||
"", | ||
...parameters.map( | ||
@@ -101,3 +228,5 @@ ({ name }) => | ||
), | ||
` res = self.seam.make_request(`, | ||
"", | ||
` ${is_none_return_type ? "" : "res = "}self.seam.make_request(`, | ||
` "POST",`, | ||
@@ -107,9 +236,37 @@ ` "${path}",`, | ||
` )`, | ||
is_return_resource_a_list | ||
? ` return [${list_item_type}.from_dict(item) for item in res["${return_path.join( | ||
'"]["' | ||
)}"]]` | ||
: ` return ${return_resource}.from_dict(res["${return_path.join( | ||
'"]["' | ||
)}"])`, | ||
"", | ||
does_method_use_action_attempt | ||
? [ | ||
" if isinstance(wait_for_action_attempt, dict):", | ||
` updated_action_attempt = self.seam.action_attempts.poll_until_ready(`, | ||
" res['action_attempt']['action_attempt_id'],", | ||
" timeout=wait_for_action_attempt.get('timeout', None),", | ||
" polling_interval=wait_for_action_attempt.get('polling_interval', None),", | ||
" )", | ||
" elif wait_for_action_attempt is True:", | ||
` updated_action_attempt = self.seam.action_attempts.poll_until_ready(`, | ||
" res['action_attempt']['action_attempt_id']", | ||
" )", | ||
" else:", | ||
` return ${return_resource}.from_dict(res["${return_path.join( | ||
'"]["' | ||
)}"])`, | ||
"", | ||
" return updated_action_attempt", | ||
].join("\n") | ||
: "", | ||
"", | ||
!does_method_use_action_attempt | ||
? is_none_return_type | ||
? ` return None` | ||
: is_return_resource_list | ||
? ` return [${return_resource_item}.from_dict(item) for item in res["${return_path.join( | ||
'"]["' | ||
)}"]]` | ||
: ` return ${return_resource}.from_dict(res["${return_path.join( | ||
'"]["' | ||
)}"])` | ||
: "", | ||
] | ||
@@ -120,4 +277,30 @@ .map((s) => ` ${s}`) | ||
), | ||
this.name === "ActionAttempts" | ||
? [ | ||
"", | ||
" def poll_until_ready(self, action_attempt_id: str, timeout: Optional[float] = 5.0, polling_interval: Optional[float] = 0.5) -> ActionAttempt:", | ||
" seam = self.seam", | ||
" time_waiting = 0.0", | ||
"", | ||
" action_attempt = seam.action_attempts.get(action_attempt_id, wait_for_action_attempt=False)", | ||
"", | ||
" while action_attempt.status == 'pending':", | ||
" time.sleep(polling_interval)", | ||
" time_waiting += polling_interval", | ||
"", | ||
" if time_waiting > timeout:", | ||
" raise Exception('Timed out waiting for action attempt to be ready')", | ||
"", | ||
" action_attempt = seam.action_attempts.get(", | ||
" action_attempt.action_attempt_id, wait_for_action_attempt=False", | ||
" )", | ||
"", | ||
" if action_attempt.status == 'failed':", | ||
" raise Exception(f'Action Attempt failed: {action_attempt.error.message}')", | ||
"", | ||
" return action_attempt", | ||
].join("\n") | ||
: "", | ||
].join("\n") | ||
} | ||
} |
@@ -17,3 +17,12 @@ import axios from "axios" | ||
import readmeMdTemplate from "./templates/readme.md.template.js" | ||
import initTemplate from "./templates/__init__.py.template.js" | ||
import gitignoreTemplate from "./templates/.gitignore.template.js" | ||
import prebuildTemplate from "./templates/prebuild.py.template.js" | ||
import reportErrorTemplate from "./templates/utils/report_error.py.template.js" | ||
import getSentryDsnTemplate from "./templates/utils/get_sentry_dsn.py.template.js" | ||
import SeamApiExceptionClassTemplate from "./templates/snippets/seam-api-exception-class.template.js" | ||
import { getParameterAndResponseSchema } from "lib/openapi/get-parameter-and-response-schema.js" | ||
import mapParentToChildrenResources from "lib/openapi/map-parent-to-children-resource.js" | ||
import { deepFlattenOneOfAndAllOfSchema } from "lib/generate-php-sdk/utils/deep-flatten-one-of-and-all-of-schema.js" | ||
import endpoints_returning_deprecated_action_attempt from "lib/endpoints-returning-deprecated-action-attempt.js" | ||
@@ -30,9 +39,22 @@ export const generatePythonSDK = async () => { | ||
const fs: any = {} | ||
const class_map: Record<string, ClassFile> = {} | ||
const namespaces: string[][] = [] | ||
const parent_to_children_resources_map = mapParentToChildrenResources(routes) | ||
fs["README.md"] = readmeMdTemplate() | ||
const processClass = (resource_name: string) => { | ||
const child_class_identifiers = ( | ||
parent_to_children_resources_map[resource_name] ?? [] | ||
).map((child_resource) => ({ | ||
class_name: pascalCase(`${resource_name} ${child_resource}`), | ||
namespace: child_resource, | ||
})) | ||
const pascal_resource_name = pascalCase(resource_name) | ||
const class_map: Record<string, ClassFile> = {} | ||
class_map[pascal_resource_name] = new ClassFile( | ||
pascal_resource_name, | ||
resource_name, | ||
child_class_identifiers | ||
) | ||
} | ||
const namespaces: string[][] = [] | ||
for (const route of routes) { | ||
@@ -42,9 +64,23 @@ if (!route.post) continue | ||
const group_names = [...route.post["x-fern-sdk-group-name"]] | ||
const [base_resource] = group_names | ||
const namespace = group_names.join("_") | ||
group_names.reverse() | ||
const class_name = pascalCase(group_names.join("_")) | ||
const class_name = pascalCase(namespace) | ||
if (!class_map[class_name]) { | ||
namespaces.push(route.post["x-fern-sdk-group-name"]) | ||
class_map[class_name] = new ClassFile(class_name, namespace) | ||
processClass(namespace) | ||
} | ||
/* | ||
special case when we don't have routes for a base resource | ||
and thus a respective x-fern-sdk-group-name for ex. /noise_sensors | ||
*/ | ||
if (base_resource && !class_map[pascalCase(base_resource)]) { | ||
namespaces.push([base_resource]) | ||
processClass(base_resource) | ||
} | ||
const cls = class_map[class_name] | ||
@@ -60,7 +96,2 @@ | ||
if (!response_obj_type && !response_arr_type) { | ||
console.warn(`No response object/array ref for "${route.path}", skipping`) | ||
continue | ||
} | ||
if (!parameter_schema) { | ||
@@ -77,7 +108,12 @@ console.warn(`No parameter schema for "${route.path}", skipping`) | ||
([_, param_val]) => | ||
param_val && typeof param_val === "object" && "type" in param_val | ||
"type" in param_val || | ||
("oneOf" in param_val && "type" in (param_val.oneOf[0] ?? {})) | ||
) | ||
.map(([param_name, param_val]: any) => ({ | ||
name: param_name, | ||
type: mapPythonType(param_val.type), | ||
type: mapPythonType( | ||
"type" in param_val | ||
? param_val | ||
: deepFlattenOneOfAndAllOfSchema(param_val) | ||
), | ||
position: | ||
@@ -88,7 +124,10 @@ route.post["x-fern-sdk-method-name"] === "get" && | ||
: undefined, | ||
required: parameter_schema.required?.includes(param_name), | ||
})), | ||
return_path: [route.post["x-fern-sdk-return-value"]], | ||
return_resource: response_obj_type | ||
? pascalCase(response_obj_type) | ||
: `List[${pascalCase(response_arr_type)}]`, | ||
return_resource: determineReturnResource({ | ||
route_path: route.path, | ||
response_obj_type, | ||
response_arr_type, | ||
}), | ||
}) | ||
@@ -128,9 +167,12 @@ } | ||
"", | ||
`class SeamAPIException(Exception):`, | ||
` pass`, | ||
SeamApiExceptionClassTemplate(), | ||
"", | ||
"", | ||
...Object.entries(class_map).map(([_, cls]) => | ||
cls.serializeToAbstractClassWithoutImports() | ||
), | ||
...Object.entries(class_map) | ||
.sort( | ||
// define classes without children first for parent-child referencing | ||
([, a], [, b]) => | ||
a.child_class_identifiers.length - b.child_class_identifiers.length | ||
) | ||
.map(([_, cls]) => cls.serializeToAbstractClassWithoutImports()), | ||
"", | ||
@@ -143,8 +185,15 @@ "", | ||
fs["seamapi/__init__.py"] = `from seamapi.seam import Seam` | ||
fs["README.md"] = readmeMdTemplate() | ||
fs["pyproject.toml"] = pyprojectTomlTemplate() | ||
fs[".gitignore"] = gitignoreTemplate() | ||
fs["scripts/prebuild.py"] = prebuildTemplate() | ||
fs["seamapi/__init__.py"] = initTemplate() | ||
fs["seamapi/routes.py"] = routesPyTemplate(top_level_namespaces) | ||
fs["seamapi/seam.py"] = seamPyTemplate() | ||
fs["seamapi/utils/deep_attr_dict.py"] = deep_attr_dictPyTemplate() | ||
fs["pyproject.toml"] = pyprojectTomlTemplate() | ||
fs["seamapi/utils/report_error.py"] = reportErrorTemplate() | ||
fs["seamapi/utils/get_sentry_dsn.py"] = getSentryDsnTemplate() | ||
fs["tests/conftest.py"] = conftestPyTemplate() | ||
@@ -156,1 +205,25 @@ fs["tests/test_smoke.py"] = test_smokePyTemplate() | ||
} | ||
function determineReturnResource({ | ||
route_path, | ||
response_arr_type, | ||
response_obj_type, | ||
}: { | ||
route_path: string | ||
response_obj_type?: string | ||
response_arr_type?: string | ||
}) { | ||
if (endpoints_returning_deprecated_action_attempt.includes(route_path)) { | ||
return "None" | ||
} | ||
if (response_obj_type) { | ||
return pascalCase(response_obj_type) | ||
} | ||
if (response_arr_type) { | ||
return `List[${pascalCase(response_arr_type)}]` | ||
} | ||
return "None" | ||
} |
@@ -1,2 +0,2 @@ | ||
import type { PropertySchema } from "lib/types.js" | ||
import type { PrimitiveSchema, PropertySchema } from "lib/types.js" | ||
@@ -23,4 +23,10 @@ // TODO literals? | ||
return "float" | ||
if ("type" in property_schema && property_schema.type === "array") | ||
return "List[Any]" // TODO, make more specific | ||
if ("type" in property_schema && property_schema.type === "array") { | ||
const array_item_type: string = | ||
"type" in property_schema.items | ||
? mapPythonType({ type: property_schema.items.type } as PrimitiveSchema) | ||
: "Any" | ||
return `List[${array_item_type}]` | ||
} // TODO, make more specific | ||
if ("type" in property_schema && property_schema.type === "object") | ||
@@ -27,0 +33,0 @@ return "Dict[str, Any]" // TODO, make more specific |
@@ -1,11 +0,80 @@ | ||
export default () => `import pytest | ||
import random | ||
export default () => `import random | ||
import string | ||
import pytest | ||
from seamapi import Seam | ||
from dotenv import load_dotenv | ||
from typing import Any | ||
from dataclasses import dataclass | ||
@pytest.fixture(autouse=True) | ||
def dotenv_fixture(): | ||
load_dotenv() | ||
@dataclass | ||
class SeamBackend: | ||
url: str | ||
sandbox_api_key: str | ||
# TODO this should use scope="session", but there's some issue, this would | ||
# dramatically reduce test time to switch | ||
@pytest.fixture(scope="function") | ||
def seam(): | ||
r = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) | ||
seam = Seam(api_url=f"https://{r}.fakeseamconnect.seam.vc", api_key="seam_apikey1_token") | ||
yield seam` | ||
def seam_backend(): | ||
random_string = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10)) | ||
yield SeamBackend( | ||
url=f"https://{random_string}.fakeseamconnect.seam.vc", | ||
sandbox_api_key="seam_apikey1_token", | ||
) | ||
@pytest.fixture | ||
def seam(seam_backend: Any): | ||
seam = Seam(api_url=seam_backend.url, api_key=seam_backend.sandbox_api_key) | ||
# seam.make_request("POST", "/workspaces/reset_sandbox") | ||
yield seam | ||
@pytest.fixture | ||
def fake_sentry(monkeypatch): | ||
sentry_dsn = "https://key@sentry.io/123" | ||
monkeypatch.setenv("SENTRY_DSN", sentry_dsn) | ||
sentry_init_args = {} | ||
sentry_capture_exception_calls = [] | ||
sentry_add_breadcrumb_calls = [] | ||
class TestSentryClient(object): | ||
def __init__(self, *args, **kwargs): | ||
sentry_init_args.update(kwargs) | ||
def set_context(self, *args, **kwargs): | ||
pass | ||
monkeypatch.setattr("sentry_sdk.Client", TestSentryClient) | ||
class TestSentryScope(object): | ||
def set_context(self, *args, **kwargs): | ||
pass | ||
class TestSentryHub(object): | ||
def __init__(self, *args, **kwargs): | ||
self.scope = TestSentryScope() | ||
def capture_exception(self, *args, **kwargs): | ||
sentry_capture_exception_calls.append((args, kwargs)) | ||
def add_breadcrumb(self, *args, **kwargs): | ||
sentry_add_breadcrumb_calls.append((args, kwargs)) | ||
monkeypatch.setattr("sentry_sdk.Hub", TestSentryHub) | ||
yield { | ||
"sentry_init_args": sentry_init_args, | ||
"sentry_capture_exception_calls": sentry_capture_exception_calls, | ||
"sentry_add_breadcrumb_calls": sentry_add_breadcrumb_calls, | ||
"sentry_dsn": sentry_dsn, | ||
} | ||
` |
export default () => `[tool.poetry] | ||
name = "seamapi" | ||
version = "2.14.0" | ||
version = "2.18.0" | ||
description = "A Python Library for Seam's API https://getseam.com" | ||
@@ -11,2 +11,4 @@ authors = ["Severin Ibarluzea <seveibar@gmail.com>"] | ||
requests = "^2.26.0" | ||
dataclasses-json = "^0.5.6" | ||
sentry-sdk = "^1.9.10" | ||
@@ -17,2 +19,3 @@ [tool.poetry.dev-dependencies] | ||
black = "^21.12b0" | ||
testcontainers = {extras = ["postgresql"], version = "^3.4.2"} | ||
responses = "^0.22.0" | ||
@@ -22,2 +25,3 @@ | ||
requires = ["poetry-core>=1.0.0"] | ||
build-backend = "poetry.core.masonry.api"` | ||
build-backend = "poetry.core.masonry.api" | ||
` |
export default () => `import os | ||
from seamapi.utils.get_sentry_dsn import get_sentry_dsn | ||
from .routes import Routes | ||
import requests | ||
from importlib.metadata import version | ||
import sentry_sdk | ||
import pkg_resources | ||
from typing import Optional, cast | ||
from .types import AbstractSeam, SeamAPIException | ||
from .types import AbstractSeam, SeamApiException | ||
class Seam(AbstractSeam): | ||
""" | ||
Initial Seam class used to interact with Seam API | ||
... | ||
Attributes | ||
---------- | ||
api_key : str | ||
API key (default None) | ||
api_url : str | ||
API url (default None) | ||
workspaces : Workspaces | ||
Workspaces class | ||
connected_accounts : ConnectedAccounts | ||
Connected accounts class | ||
connect_webviews : ConnectWebviews | ||
Connect webviews class | ||
devices : Devices | ||
Devices class | ||
events : Events | ||
Events class | ||
locks : Locks | ||
Locks class | ||
access_codes : AccessCodes | ||
Access codes class | ||
action_attempts : ActionAttempts | ||
Action attempts class | ||
""" | ||
api_key: str | ||
@@ -17,2 +48,3 @@ api_url: str = "https://connect.getseam.com" | ||
api_key: Optional[str] = None, | ||
workspace_id: Optional[str] = None, | ||
api_url: Optional[str] = None, | ||
@@ -26,4 +58,8 @@ should_report_exceptions: Optional[bool] = False, | ||
API key | ||
workspace_id : str, optional | ||
Workspace id | ||
api_url : str, optional | ||
API url | ||
should_report_exceptions : bool, optional | ||
Defaults to False. If true, thrown exceptions will be reported to Seam. | ||
""" | ||
@@ -38,7 +74,31 @@ Routes.__init__(self) | ||
) | ||
if api_url is None: | ||
api_url = os.environ.get("SEAM_API_URL", self.api_url) | ||
if workspace_id is None: | ||
workspace_id = os.environ.get("SEAM_WORKSPACE_ID", None) | ||
self.api_key = api_key | ||
self.api_url = cast(str, api_url) | ||
self.workspace_id = workspace_id | ||
if os.environ.get("SEAM_API_URL", None) is not None: | ||
print( | ||
'\\n' | ||
'\\033[93m' | ||
'Using the SEAM_API_URL environment variable is deprecated. ' | ||
'Support will be removed in a later major version. Use SEAM_ENDPOINT instead.' | ||
'\\033[0m' | ||
) | ||
api_url = os.environ.get("SEAM_API_URL", None) or os.environ.get("SEAM_ENDPOINT", None) or api_url | ||
if api_url is not None: | ||
self.api_url = cast(str, api_url) | ||
self.should_report_exceptions = should_report_exceptions | ||
if self.should_report_exceptions: | ||
self.sentry_client = sentry_sdk.Hub(sentry_sdk.Client( | ||
dsn=get_sentry_dsn(), | ||
)) | ||
self.sentry_client.scope.set_context("sdk_info", { | ||
"repository": "https://github.com/seamapi/python", | ||
"version": pkg_resources.get_distribution("seamapi").version, | ||
"endpoint": self.api_url, | ||
}) | ||
def make_request(self, method: str, path: str, **kwargs): | ||
@@ -59,19 +119,35 @@ """ | ||
url = self.api_url + path | ||
sdk_version = pkg_resources.get_distribution("seamapi").version | ||
headers = { | ||
"Authorization": "Bearer " + self.api_key, | ||
"Content-Type": "application/json", | ||
"User-Agent": "Python SDK v" + version("seamapi") + " (https://github.com/seamapi/python)", | ||
"User-Agent": "Python SDK v" + sdk_version + " (https://github.com/seamapi/python)", | ||
"seam-sdk-name": "seamapi/python", | ||
"seam-sdk-version": sdk_version, | ||
} | ||
if self.workspace_id is not None: | ||
headers["seam-workspace"] = self.workspace_id | ||
response = requests.request(method, url, headers=headers, **kwargs) | ||
parsed_response = response.json() | ||
if self.should_report_exceptions and response.status_code: | ||
# Add breadcrumb | ||
self.sentry_client.add_breadcrumb( | ||
category="http", | ||
level="info", | ||
data={ | ||
"method": method, | ||
"url": url, | ||
"status_code": response.status_code, | ||
"request_id": response.headers.get("seam-request-id", "unknown"), | ||
}, | ||
) | ||
if response.status_code != 200: | ||
raise SeamAPIException( | ||
response.status_code, | ||
response.headers.get("seam-request-id", None), | ||
parsed_response["error"], | ||
) | ||
## TODO automatically paginate if kwargs["auto_paginate"] is True | ||
raise SeamApiException(response) | ||
return parsed_response` | ||
if "application/json" in response.headers["content-type"]: | ||
return response.json() | ||
return response.text | ||
` |
export default () => `@dataclass | ||
class AbstractSeam(AbstractRoutes): | ||
api_key: str | ||
api_url: str | ||
api_key: str | ||
workspace_id: str | ||
api_url: str | ||
@abc.abstractmethod | ||
def __init__(self, api_key: Optional[str] = None): | ||
raise NotImplementedError` | ||
@abc.abstractmethod | ||
def __init__( | ||
self, | ||
api_key: Optional[str] = None, | ||
workspace_id: Optional[str] = None, | ||
api_url: Optional[str] = None, | ||
should_report_exceptions: Optional[bool] = False, | ||
): | ||
raise NotImplementedError` |
@@ -14,3 +14,11 @@ export default ( | ||
return ${name}( | ||
${parameters.map((p) => ` ${p.name}=d.get("${p.name}", None),`).join("\n")} | ||
${parameters | ||
.map((p) => { | ||
const is_dict_param = p.type.startsWith("Dict") || p.name === "properties" | ||
return ` ${p.name}=${is_dict_param ? "DeepAttrDict(" : ""}d.get("${ | ||
p.name | ||
}", None)${is_dict_param ? ")" : ""},` | ||
}) | ||
.join("\n")} | ||
)` |
@@ -47,14 +47,11 @@ import type { ObjSchema, Route } from "lib/types.js" | ||
} | ||
} else { | ||
return { | ||
response_obj_type: response_obj_ref?.split("/")?.pop(), | ||
response_arr_type: response_arr_ref?.split("/")?.pop(), | ||
parameter_schema, | ||
res_return_schema: res_return_schema as ObjSchema | undefined, | ||
nullable, | ||
} | ||
} | ||
const response_obj_type = response_obj_ref?.split("/")?.pop()! | ||
const response_arr_type = response_arr_ref?.split("/")?.pop()! | ||
return { | ||
response_obj_type, | ||
response_arr_type, | ||
parameter_schema, | ||
res_return_schema: res_return_schema as ObjSchema | undefined, | ||
nullable, | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
692263
347
9315