├── backend ├── tests │ ├── __init__.py │ └── placeholder_test.py ├── src │ └── fern_fastapi_starter │ │ ├── __init__.py │ │ ├── api │ │ ├── __init__.py │ │ ├── generated │ │ │ ├── core │ │ │ │ ├── security │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── bearer.py │ │ │ │ ├── abstract_fern_service.py │ │ │ │ ├── exceptions │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── unauthorized.py │ │ │ │ │ ├── fern_http_exception.py │ │ │ │ │ └── handlers.py │ │ │ │ ├── __init__.py │ │ │ │ ├── datetime_utils.py │ │ │ │ └── route_args.py │ │ │ ├── resources │ │ │ │ ├── imdb │ │ │ │ │ ├── service │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── service.py │ │ │ │ │ ├── types │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ ├── movie.py │ │ │ │ │ │ └── movie_id.py │ │ │ │ │ ├── errors │ │ │ │ │ │ ├── __init__.py │ │ │ │ │ │ └── movie_does_not_exist_error.py │ │ │ │ │ └── __init__.py │ │ │ │ └── __init__.py │ │ │ ├── __init__.py │ │ │ └── register.py │ │ └── README.md │ │ ├── movies_service.py │ │ └── server.py ├── .flake8 ├── mypy.ini ├── Dockerfile ├── pyproject.toml └── requirements.txt ├── frontend ├── .prettierignore ├── src │ ├── api │ │ ├── index.ts │ │ ├── generated │ │ │ ├── api │ │ │ │ ├── index.js │ │ │ │ ├── index.d.ts │ │ │ │ └── resources │ │ │ │ │ ├── imdb │ │ │ │ │ ├── client │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── Client.d.ts │ │ │ │ │ │ └── Client.js │ │ │ │ │ ├── errors │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ ├── MovieDoesNotExistError.d.ts │ │ │ │ │ │ └── MovieDoesNotExistError.js │ │ │ │ │ ├── types │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── Movie.js │ │ │ │ │ │ ├── MovieId.js │ │ │ │ │ │ ├── MovieId.d.ts │ │ │ │ │ │ └── Movie.d.ts │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.d.ts │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ ├── core │ │ │ │ ├── fetcher │ │ │ │ │ ├── APIResponse.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── Supplier.d.ts │ │ │ │ │ ├── Supplier.js │ │ │ │ │ ├── APIResponse.d.ts │ │ │ │ │ ├── Fetcher.d.ts │ │ │ │ │ └── Fetcher.js │ │ │ │ ├── schemas │ │ │ │ │ ├── builders │ │ │ │ │ │ ├── union │ │ │ │ │ │ │ ├── types.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── discriminant.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── union.d.ts │ │ │ │ │ │ │ ├── discriminant.d.ts │ │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ │ └── union.js │ │ │ │ │ │ ├── object │ │ │ │ │ │ │ ├── types.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── property.js │ │ │ │ │ │ │ ├── object.d.ts │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── property.d.ts │ │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ │ └── object.js │ │ │ │ │ │ ├── record │ │ │ │ │ │ │ ├── types.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── record.d.ts │ │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ │ └── record.js │ │ │ │ │ │ ├── object-like │ │ │ │ │ │ │ ├── types.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ │ ├── getObjectLikeUtils.d.ts │ │ │ │ │ │ │ └── getObjectLikeUtils.js │ │ │ │ │ │ ├── date │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── date.d.ts │ │ │ │ │ │ │ └── date.js │ │ │ │ │ │ ├── enum │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── enum.d.ts │ │ │ │ │ │ │ └── enum.js │ │ │ │ │ │ ├── list │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── list.d.ts │ │ │ │ │ │ │ └── list.js │ │ │ │ │ │ ├── set │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── set.d.ts │ │ │ │ │ │ │ └── set.js │ │ │ │ │ │ ├── undiscriminated-union │ │ │ │ │ │ │ ├── types.js │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── undiscriminatedUnion.d.ts │ │ │ │ │ │ │ ├── types.d.ts │ │ │ │ │ │ │ └── undiscriminatedUnion.js │ │ │ │ │ │ ├── literals │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── stringLiteral.d.ts │ │ │ │ │ │ │ └── stringLiteral.js │ │ │ │ │ │ ├── lazy │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── lazyObject.d.ts │ │ │ │ │ │ │ ├── lazy.d.ts │ │ │ │ │ │ │ ├── lazyObject.js │ │ │ │ │ │ │ └── lazy.js │ │ │ │ │ │ ├── primitives │ │ │ │ │ │ │ ├── any.d.ts │ │ │ │ │ │ │ ├── number.d.ts │ │ │ │ │ │ │ ├── string.d.ts │ │ │ │ │ │ │ ├── boolean.d.ts │ │ │ │ │ │ │ ├── unknown.d.ts │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── any.js │ │ │ │ │ │ │ ├── unknown.js │ │ │ │ │ │ │ ├── number.js │ │ │ │ │ │ │ ├── string.js │ │ │ │ │ │ │ └── boolean.js │ │ │ │ │ │ ├── schema-utils │ │ │ │ │ │ │ ├── stringifyValidationErrors.d.ts │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ │ ├── JsonError.d.ts │ │ │ │ │ │ │ ├── ParseError.d.ts │ │ │ │ │ │ │ ├── stringifyValidationErrors.js │ │ │ │ │ │ │ ├── JsonError.js │ │ │ │ │ │ │ ├── ParseError.js │ │ │ │ │ │ │ ├── getSchemaUtils.d.ts │ │ │ │ │ │ │ └── getSchemaUtils.js │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── MaybePromise.js │ │ │ │ │ │ ├── addQuestionMarksToNullableProperties.js │ │ │ │ │ │ ├── keys.d.ts │ │ │ │ │ │ ├── MaybePromise.d.ts │ │ │ │ │ │ ├── keys.js │ │ │ │ │ │ ├── entries.d.ts │ │ │ │ │ │ ├── entries.js │ │ │ │ │ │ ├── filterObject.d.ts │ │ │ │ │ │ ├── partition.d.ts │ │ │ │ │ │ ├── isPlainObject.d.ts │ │ │ │ │ │ ├── createIdentitySchemaCreator.d.ts │ │ │ │ │ │ ├── partition.js │ │ │ │ │ │ ├── filterObject.js │ │ │ │ │ │ ├── addQuestionMarksToNullableProperties.d.ts │ │ │ │ │ │ ├── createIdentitySchemaCreator.js │ │ │ │ │ │ └── isPlainObject.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.d.ts │ │ │ │ │ ├── Schema.js │ │ │ │ │ └── Schema.d.ts │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── serialization │ │ │ │ ├── index.js │ │ │ │ ├── index.d.ts │ │ │ │ └── resources │ │ │ │ │ ├── imdb │ │ │ │ │ ├── index.js │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── types │ │ │ │ │ │ ├── index.d.ts │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── MovieId.js │ │ │ │ │ │ ├── Movie.js │ │ │ │ │ │ ├── MovieId.d.ts │ │ │ │ │ │ └── Movie.d.ts │ │ │ │ │ ├── index.d.ts │ │ │ │ │ └── index.js │ │ │ ├── errors │ │ │ │ ├── index.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── ImdbApiTimeoutError.d.ts │ │ │ │ ├── ImdbApiTimeoutError.js │ │ │ │ ├── ImdbApiError.d.ts │ │ │ │ └── ImdbApiError.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── Client.js │ │ │ └── Client.d.ts │ │ └── README.md │ ├── vite-env.d.ts │ ├── App.css │ ├── main.tsx │ ├── App.tsx │ └── index.css ├── .gitignore ├── .vscode │ ├── extensions.json │ └── settings.json ├── vite.config.ts ├── .prettierrc.json ├── dist │ ├── assets │ │ ├── index-bc8ef093.js │ │ └── index-938200cc.css │ ├── index.html │ └── vite.svg ├── tsconfig.node.json ├── index.html ├── tsconfig.json ├── .eslintrc.cjs ├── package.json └── public │ └── vite.svg ├── .gitignore ├── fern ├── fern.config.json └── api │ ├── definition │ ├── api.yml │ └── imdb.yml │ └── generators.yml ├── assets ├── frontend-demo.gif ├── backend-mypy-error.png └── frontend-autocomplete.png ├── .vscode ├── extensions.json └── settings.json ├── .github ├── dependabot.yml └── workflows │ ├── fern.yml │ ├── frontend.yml │ ├── stale-bot.yml │ ├── dependabot.yml │ └── backend.yml ├── openapi └── openapi.yml └── README.md /backend/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.prettierignore: -------------------------------------------------------------------------------- 1 | src/api -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache/ 2 | __pycache__/ 3 | .DS_Store -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generated"; 2 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/index.js: -------------------------------------------------------------------------------- 1 | export * from "./resources"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/APIResponse.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./resources"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/client/index.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/client/index.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/MaybePromise.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /backend/tests/placeholder_test.py: -------------------------------------------------------------------------------- 1 | def test_placeholder() -> None: 2 | pass 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/index.js: -------------------------------------------------------------------------------- 1 | export * from "./builders"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/index.js: -------------------------------------------------------------------------------- 1 | export * from "./resources"; 2 | -------------------------------------------------------------------------------- /fern/fern.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization": "imdb", 3 | "version": "0.6.5-rc1" 4 | } -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./resources"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/index.js: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/date/index.js: -------------------------------------------------------------------------------- 1 | export { date } from "./date"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/enum/index.js: -------------------------------------------------------------------------------- 1 | export { enum_ } from "./enum"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/list/index.js: -------------------------------------------------------------------------------- 1 | export { list } from "./list"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/set/index.d.ts: -------------------------------------------------------------------------------- 1 | export { set } from "./set"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/set/index.js: -------------------------------------------------------------------------------- 1 | export { set } from "./set"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/date/index.d.ts: -------------------------------------------------------------------------------- 1 | export { date } from "./date"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/enum/index.d.ts: -------------------------------------------------------------------------------- 1 | export { enum_ } from "./enum"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/list/index.d.ts: -------------------------------------------------------------------------------- 1 | export { list } from "./list"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/index.js: -------------------------------------------------------------------------------- 1 | export { record } from "./record"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/addQuestionMarksToNullableProperties.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /assets/frontend-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fern-api/fastapi-starter/HEAD/assets/frontend-demo.gif -------------------------------------------------------------------------------- /backend/.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = src/fern_fastapi_starter/api/generated 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/errors/index.js: -------------------------------------------------------------------------------- 1 | export * from "./MovieDoesNotExistError"; 2 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/__init__.py: -------------------------------------------------------------------------------- 1 | from . import generated 2 | 3 | __all__ = ["generated"] 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/errors/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./MovieDoesNotExistError"; 2 | -------------------------------------------------------------------------------- /assets/backend-mypy-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fern-api/fastapi-starter/HEAD/assets/backend-mypy-error.png -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/keys.d.ts: -------------------------------------------------------------------------------- 1 | export declare function keys(object: T): (keyof T)[]; 2 | -------------------------------------------------------------------------------- /fern/api/definition/api.yml: -------------------------------------------------------------------------------- 1 | name: api 2 | error-discrimination: 3 | strategy: property 4 | property-name: error 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./MovieId"; 2 | export * from "./Movie"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/index.js: -------------------------------------------------------------------------------- 1 | export * from "./MovieId"; 2 | export * from "./Movie"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as serialization from "./schemas"; 2 | export * from "./fetcher"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/index.js: -------------------------------------------------------------------------------- 1 | export * as serialization from "./schemas"; 2 | export * from "./fetcher"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/literals/index.d.ts: -------------------------------------------------------------------------------- 1 | export { stringLiteral } from "./stringLiteral"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/literals/index.js: -------------------------------------------------------------------------------- 1 | export { stringLiteral } from "./stringLiteral"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/MaybePromise.d.ts: -------------------------------------------------------------------------------- 1 | export declare type MaybePromise = T | Promise; 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "matangover.mypy" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /assets/frontend-autocomplete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fern-api/fastapi-starter/HEAD/assets/frontend-autocomplete.png -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/index.js: -------------------------------------------------------------------------------- 1 | export { fetcher } from "./Fetcher"; 2 | export { Supplier } from "./Supplier"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/keys.js: -------------------------------------------------------------------------------- 1 | export function keys(object) { 2 | return Object.keys(object); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./MovieId"; 2 | export * from "./Movie"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/index.js: -------------------------------------------------------------------------------- 1 | export * from "./MovieId"; 2 | export * from "./Movie"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as imdb from "./imdb"; 2 | export * from "./imdb/types"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/index.js: -------------------------------------------------------------------------------- 1 | export * as imdb from "./imdb"; 2 | export * from "./imdb/types"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/entries.d.ts: -------------------------------------------------------------------------------- 1 | export declare function entries(object: T): [keyof T, T[keyof T]][]; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/index.js: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./errors"; 3 | export * from "./client"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/entries.js: -------------------------------------------------------------------------------- 1 | export function entries(object) { 2 | return Object.entries(object); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./errors"; 3 | export * from "./client"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/index.js: -------------------------------------------------------------------------------- 1 | export { lazy } from "./lazy"; 2 | export { lazyObject } from "./lazyObject"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/any.d.ts: -------------------------------------------------------------------------------- 1 | export declare const any: () => import("../../Schema").Schema; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/index.js: -------------------------------------------------------------------------------- 1 | export { undiscriminatedUnion } from "./undiscriminatedUnion"; 2 | -------------------------------------------------------------------------------- /backend/mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = src 3 | mypy_path = src 4 | 5 | plugins = pydantic.mypy 6 | 7 | [mypy-uvicorn.*] 8 | ignore_missing_imports = True -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/index.js: -------------------------------------------------------------------------------- 1 | export { discriminant } from "./discriminant"; 2 | export { union } from "./union"; 3 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/cache 3 | !.yarn/patches 4 | !.yarn/plugins 5 | !.yarn/releases 6 | !.yarn/sdks 7 | !.yarn/versions 8 | node_modules -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as imdb from "./imdb"; 2 | export * from "./imdb/types"; 3 | export * from "./imdb/errors"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/index.js: -------------------------------------------------------------------------------- 1 | export * as imdb from "./imdb"; 2 | export * from "./imdb/types"; 3 | export * from "./imdb/errors"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/index.js: -------------------------------------------------------------------------------- 1 | export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/number.d.ts: -------------------------------------------------------------------------------- 1 | export declare const number: () => import("../../Schema").Schema; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/string.d.ts: -------------------------------------------------------------------------------- 1 | export declare const string: () => import("../../Schema").Schema; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/index.js: -------------------------------------------------------------------------------- 1 | export { ImdbApiError } from "./ImdbApiError"; 2 | export { ImdbApiTimeoutError } from "./ImdbApiTimeoutError"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/Movie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export {}; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/MovieId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export {}; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/boolean.d.ts: -------------------------------------------------------------------------------- 1 | export declare const boolean: () => import("../../Schema").Schema; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/unknown.d.ts: -------------------------------------------------------------------------------- 1 | export declare const unknown: () => import("../../Schema").Schema; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/index.d.ts: -------------------------------------------------------------------------------- 1 | export { ImdbApiError } from "./ImdbApiError"; 2 | export { ImdbApiTimeoutError } from "./ImdbApiTimeoutError"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/date/date.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function date(): Schema; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/index.d.ts: -------------------------------------------------------------------------------- 1 | export { lazy, type SchemaGetter } from "./lazy"; 2 | export { lazyObject } from "./lazyObject"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/filterObject.d.ts: -------------------------------------------------------------------------------- 1 | export declare function filterObject(obj: T, keysToInclude: K[]): Pick; 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/partition.d.ts: -------------------------------------------------------------------------------- 1 | export declare function partition(items: readonly T[], predicate: (item: T) => boolean): [T[], T[]]; 2 | -------------------------------------------------------------------------------- /frontend/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/index.js: -------------------------------------------------------------------------------- 1 | export { getObjectUtils, object } from "./object"; 2 | export { isProperty, property } from "./property"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/index.d.ts: -------------------------------------------------------------------------------- 1 | export { record } from "./record"; 2 | export { type BaseRecordSchema, type RecordSchema } from "./types"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./builders"; 2 | export { type inferParsed, type inferRaw, type Schema, type SchemaOptions } from "./Schema"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/index.d.ts: -------------------------------------------------------------------------------- 1 | export * as ImdbApi from "./api"; 2 | export { ImdbApiClient } from "./Client"; 3 | export { ImdbApiError, ImdbApiTimeoutError } from "./errors"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/index.js: -------------------------------------------------------------------------------- 1 | export * as ImdbApi from "./api"; 2 | export { ImdbApiClient } from "./Client"; 3 | export { ImdbApiError, ImdbApiTimeoutError } from "./errors"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/MovieId.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export declare type MovieId = string; 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "python.formatting.provider": "black", 4 | "python.linting.flake8Enabled": true, 5 | "mypy.configFile": "backend/mypy.ini" 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/list/list.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function list(schema: Schema): Schema; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/literals/stringLiteral.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function stringLiteral(literal: V): Schema; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/set/set.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function set(schema: Schema): Schema>; 3 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/security/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .bearer import BearerToken 4 | 5 | __all__ = ["BearerToken"] 6 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | } 6 | 7 | .button-container { 8 | display: flex; 9 | justify-content: center; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/enum/enum.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function enum_(values: E): Schema; 3 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/index.d.ts: -------------------------------------------------------------------------------- 1 | export { type APIResponse } from "./APIResponse"; 2 | export { fetcher, type Fetcher, type FetchFunction } from "./Fetcher"; 3 | export { Supplier } from "./Supplier"; 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" # See documentation for possible values 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/Supplier.d.ts: -------------------------------------------------------------------------------- 1 | export declare type Supplier = T | Promise | (() => T | Promise); 2 | export declare const Supplier: { 3 | get: (supplier: Supplier) => Promise; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/index.d.ts: -------------------------------------------------------------------------------- 1 | export { getObjectLikeUtils, withParsedProperties } from "./getObjectLikeUtils"; 2 | export { type ObjectLikeSchema, type ObjectLikeUtils } from "./types"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/stringifyValidationErrors.d.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "../../Schema"; 2 | export declare function stringifyValidationError(error: ValidationError): string; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/isPlainObject.d.ts: -------------------------------------------------------------------------------- 1 | export declare const NOT_AN_OBJECT_ERROR_MESSAGE = "Not an object"; 2 | export declare function isPlainObject(value: unknown): value is Record; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/ImdbApiTimeoutError.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export declare class ImdbApiTimeoutError extends Error { 5 | constructor(); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/index.js: -------------------------------------------------------------------------------- 1 | export { getSchemaUtils, optional, transform } from "./getSchemaUtils"; 2 | export { JsonError } from "./JsonError"; 3 | export { ParseError } from "./ParseError"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/discriminant.js: -------------------------------------------------------------------------------- 1 | export function discriminant(parsedDiscriminant, rawDiscriminant) { 2 | return { 3 | parsedDiscriminant, 4 | rawDiscriminant, 5 | }; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/service/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .service import AbstractImdbService 4 | 5 | __all__ = ["AbstractImdbService"] 6 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "overrides": [ 5 | { 6 | "files": "*.{yml,yaml,json,md,mdx}", 7 | "options": { 8 | "tabWidth": 2 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /frontend/dist/assets/index-bc8ef093.js: -------------------------------------------------------------------------------- 1 | import{M as e,a as o}from"./index-79ef6011.js";const r=Object.freeze(Object.defineProperty({__proto__:null,Movie:e,MovieId:o},Symbol.toStringTag,{value:"Module"}));export{e as Movie,o as MovieId,r as imdb}; 2 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/types/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .movie import Movie 4 | from .movie_id import MovieId 5 | 6 | __all__ = ["Movie", "MovieId"] 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/index.d.ts: -------------------------------------------------------------------------------- 1 | export { getSchemaUtils, optional, transform, type SchemaUtils } from "./getSchemaUtils"; 2 | export { JsonError } from "./JsonError"; 3 | export { ParseError } from "./ParseError"; 4 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/index.d.ts: -------------------------------------------------------------------------------- 1 | export { any } from "./any"; 2 | export { boolean } from "./boolean"; 3 | export { number } from "./number"; 4 | export { string } from "./string"; 5 | export { unknown } from "./unknown"; 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/index.js: -------------------------------------------------------------------------------- 1 | export { any } from "./any"; 2 | export { boolean } from "./boolean"; 3 | export { number } from "./number"; 4 | export { string } from "./string"; 5 | export { unknown } from "./unknown"; 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/MovieId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as core from "../../../../core"; 5 | export const MovieId = core.serialization.string(); 6 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/errors/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .movie_does_not_exist_error import MovieDoesNotExistError 4 | 5 | __all__ = ["MovieDoesNotExistError"] 6 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .resources import Movie, MovieDoesNotExistError, MovieId, imdb 4 | 5 | __all__ = ["Movie", "MovieDoesNotExistError", "MovieId", "imdb"] 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/JsonError.d.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "../../Schema"; 2 | export declare class JsonError extends Error { 3 | readonly errors: ValidationError[]; 4 | constructor(errors: ValidationError[]); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/ParseError.d.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "../../Schema"; 2 | export declare class ParseError extends Error { 3 | readonly errors: ValidationError[]; 4 | constructor(errors: ValidationError[]); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/lazyObject.d.ts: -------------------------------------------------------------------------------- 1 | import { ObjectSchema } from "../object/types"; 2 | import { SchemaGetter } from "./lazy"; 3 | export declare function lazyObject(getter: SchemaGetter>): ObjectSchema; 4 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .errors import MovieDoesNotExistError 4 | from .types import Movie, MovieId 5 | 6 | __all__ = ["Movie", "MovieDoesNotExistError", "MovieId"] 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/stringifyValidationErrors.js: -------------------------------------------------------------------------------- 1 | export function stringifyValidationError(error) { 2 | if (error.path.length === 0) { 3 | return error.message; 4 | } 5 | return `${error.path.join(" -> ")}: ${error.message}`; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from . import imdb 4 | from .imdb import Movie, MovieDoesNotExistError, MovieId 5 | 6 | __all__ = ["Movie", "MovieDoesNotExistError", "MovieId", "imdb"] 7 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/index.d.ts: -------------------------------------------------------------------------------- 1 | export { type inferParsedUnidiscriminatedUnionSchema, type inferRawUnidiscriminatedUnionSchema, type UndiscriminatedUnionSchema, } from "./types"; 2 | export { undiscriminatedUnion } from "./undiscriminatedUnion"; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/any.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export const any = createIdentitySchemaCreator(SchemaType.ANY, (value) => ({ ok: true, value })); 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/Supplier.js: -------------------------------------------------------------------------------- 1 | export const Supplier = { 2 | get: async (supplier) => { 3 | if (typeof supplier === "function") { 4 | return supplier(); 5 | } 6 | else { 7 | return supplier; 8 | } 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/unknown.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export const unknown = createIdentitySchemaCreator(SchemaType.UNKNOWN, (value) => ({ ok: true, value })); 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/index.d.ts: -------------------------------------------------------------------------------- 1 | export { discriminant, type Discriminant } from "./discriminant"; 2 | export { type inferParsedDiscriminant, type inferParsedUnion, type inferRawDiscriminant, type inferRawUnion, type UnionSubtypes, } from "./types"; 3 | export { union } from "./union"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/createIdentitySchemaCreator.d.ts: -------------------------------------------------------------------------------- 1 | import { MaybeValid, Schema, SchemaOptions, SchemaType } from "../Schema"; 2 | export declare function createIdentitySchemaCreator(schemaType: SchemaType, validate: (value: unknown, opts?: SchemaOptions) => MaybeValid): () => Schema; 3 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/APIResponse.d.ts: -------------------------------------------------------------------------------- 1 | export declare type APIResponse = SuccessfulResponse | FailedResponse; 2 | export interface SuccessfulResponse { 3 | ok: true; 4 | body: T; 5 | } 6 | export interface FailedResponse { 7 | ok: false; 8 | error: T; 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/ImdbApiTimeoutError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export class ImdbApiTimeoutError extends Error { 5 | constructor() { 6 | super("Timeout"); 7 | Object.setPrototypeOf(this, ImdbApiTimeoutError.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/abstract_fern_service.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import abc 4 | 5 | import fastapi 6 | 7 | 8 | class AbstractFernService(abc.ABC): 9 | @classmethod 10 | def _init_fern(cls, router: fastapi.APIRouter) -> None: 11 | ... 12 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.14 2 | 3 | COPY pyproject.toml ./pyproject.toml 4 | COPY poetry.lock ./poetry.lock 5 | COPY ./src ./src 6 | 7 | ENV PYTHONPATH=${PYTHONPATH}:${PWD} 8 | 9 | RUN pip3 install poetry &&\ 10 | poetry config virtualenvs.create false &&\ 11 | poetry install 12 | 13 | ENTRYPOINT ["poetry", "run", "start"] 14 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/types/Movie.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import { ImdbApi } from "../../../.."; 5 | export interface Movie { 6 | id: ImdbApi.MovieId; 7 | title: string; 8 | /** The rating scale is one to five stars */ 9 | rating: number; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/api/README.md: -------------------------------------------------------------------------------- 1 | ## Generated code 2 | 3 | This directory contains the code that's generated by [Fern](https://github.com/fern-api/fern). 4 | The definition lives in the [fern](../../../../fern) directory. 5 | 6 | You can re-generate the `generated/` directory using the Fern CLI: 7 | 8 | ``` 9 | npm install -g fern-api 10 | fern generate 11 | ``` 12 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.yarn": true, 4 | "**/.pnp.*": true 5 | }, 6 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 7 | "typescript.enablePromptUseWorkspaceTsdk": true, 8 | "editor.formatOnSave": true, 9 | "prettier.prettierPath": ".yarn/sdks/prettier/index.js", 10 | "eslint.nodePath": ".yarn/sdks" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/errors/MovieDoesNotExistError.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as errors from "../../../../errors"; 5 | import { ImdbApi } from "../../../.."; 6 | export declare class MovieDoesNotExistError extends errors.ImdbApiError { 7 | constructor(body: ImdbApi.MovieId); 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/README.md: -------------------------------------------------------------------------------- 1 | ## Generated code 2 | 3 | This directory contains the code that's generated by [Fern](https://github.com/fern-api/fern). 4 | The definition lives in the [fern](../../../../fern) directory. 5 | 6 | You can re-generate the `generated/` directory using the Fern CLI: 7 | 8 | ``` 9 | npm install -g fern-api 10 | fern generate 11 | ``` 12 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/property.js: -------------------------------------------------------------------------------- 1 | export function property(rawKey, valueSchema) { 2 | return { 3 | rawKey, 4 | valueSchema, 5 | isProperty: true, 6 | }; 7 | } 8 | export function isProperty(maybeProperty) { 9 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 10 | return maybeProperty.isProperty; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/record.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | import { RecordSchema } from "./types"; 3 | export declare function record(keySchema: Schema, valueSchema: Schema): RecordSchema; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/partition.js: -------------------------------------------------------------------------------- 1 | export function partition(items, predicate) { 2 | const trueItems = [], falseItems = []; 3 | for (const item of items) { 4 | if (predicate(item)) { 5 | trueItems.push(item); 6 | } 7 | else { 8 | falseItems.push(item); 9 | } 10 | } 11 | return [trueItems, falseItems]; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/JsonError.js: -------------------------------------------------------------------------------- 1 | import { stringifyValidationError } from "./stringifyValidationErrors"; 2 | export class JsonError extends Error { 3 | errors; 4 | constructor(errors) { 5 | super(errors.map(stringifyValidationError).join("; ")); 6 | this.errors = errors; 7 | Object.setPrototypeOf(this, JsonError.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/ParseError.js: -------------------------------------------------------------------------------- 1 | import { stringifyValidationError } from "./stringifyValidationErrors"; 2 | export class ParseError extends Error { 3 | errors; 4 | constructor(errors) { 5 | super(errors.map(stringifyValidationError).join("; ")); 6 | this.errors = errors; 7 | Object.setPrototypeOf(this, ParseError.prototype); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/generated/Client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import { Imdb } from "./api/resources/imdb/client/Client"; 5 | export class ImdbApiClient { 6 | options; 7 | constructor(options) { 8 | this.options = options; 9 | } 10 | _imdb; 11 | get imdb() { 12 | return (this._imdb ??= new Imdb(this.options)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/ImdbApiError.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export declare class ImdbApiError extends Error { 5 | readonly statusCode?: number; 6 | readonly body?: unknown; 7 | constructor({ message, statusCode, body }: { 8 | message?: string; 9 | statusCode?: number; 10 | body?: unknown; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | 6 | const root = document.getElementById("root"); 7 | 8 | if (root == null) { 9 | throw new Error("No element with ID #root."); 10 | } 11 | 12 | ReactDOM.createRoot(root).render( 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fern starter 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/union.d.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLikeSchema } from "../object-like"; 2 | import { Discriminant } from "./discriminant"; 3 | import { inferParsedUnion, inferRawUnion, UnionSubtypes } from "./types"; 4 | export declare function union, U extends UnionSubtypes>(discriminant: D, union: U): ObjectLikeSchema, inferParsedUnion>; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/Movie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as core from "../../../../core"; 5 | export const Movie = core.serialization.object({ 6 | id: core.serialization.lazy(async () => (await import("../../..")).MovieId), 7 | title: core.serialization.string(), 8 | rating: core.serialization.number(), 9 | }); 10 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/movies_service.py: -------------------------------------------------------------------------------- 1 | from .api.generated.resources.imdb.service.service import AbstractImdbService 2 | from .api.generated import Movie, MovieId 3 | 4 | 5 | class MoviesService(AbstractImdbService): 6 | def get_movie(self, *, movie_id: str) -> Movie: 7 | return Movie( 8 | title="Goodwill Hunting", 9 | rating=9.8, 10 | id=MovieId.from_str("tt0119217"), 11 | ) 12 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/object.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseObjectSchema, inferObjectSchemaFromPropertySchemas, ObjectUtils, PropertySchemas } from "./types"; 2 | export declare function object>(schemas: T): inferObjectSchemaFromPropertySchemas; 3 | export declare function getObjectUtils(schema: BaseObjectSchema): ObjectUtils; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | import { inferParsedUnidiscriminatedUnionSchema, inferRawUnidiscriminatedUnionSchema } from "./types"; 3 | export declare function undiscriminatedUnion, ...Schema[]]>(schemas: Schemas): Schema, inferParsedUnidiscriminatedUnionSchema>; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/filterObject.js: -------------------------------------------------------------------------------- 1 | export function filterObject(obj, keysToInclude) { 2 | const keysToIncludeSet = new Set(keysToInclude); 3 | return Object.entries(obj).reduce((acc, [key, value]) => { 4 | if (keysToIncludeSet.has(key)) { 5 | acc[key] = value; 6 | } 7 | return acc; 8 | // eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter 9 | }, {}); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./date"; 2 | export * from "./enum"; 3 | export * from "./lazy"; 4 | export * from "./list"; 5 | export * from "./literals"; 6 | export * from "./object"; 7 | export * from "./object-like"; 8 | export * from "./primitives"; 9 | export * from "./record"; 10 | export * from "./schema-utils"; 11 | export * from "./set"; 12 | export * from "./undiscriminated-union"; 13 | export * from "./union"; 14 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/index.js: -------------------------------------------------------------------------------- 1 | export * from "./date"; 2 | export * from "./enum"; 3 | export * from "./lazy"; 4 | export * from "./list"; 5 | export * from "./literals"; 6 | export * from "./object"; 7 | export * from "./object-like"; 8 | export * from "./primitives"; 9 | export * from "./record"; 10 | export * from "./schema-utils"; 11 | export * from "./set"; 12 | export * from "./undiscriminated-union"; 13 | export * from "./union"; 14 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/MovieId.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as serializers from "../../.."; 5 | import { ImdbApi } from "../../../.."; 6 | import * as core from "../../../../core"; 7 | export declare const MovieId: core.serialization.Schema; 8 | export declare namespace MovieId { 9 | type Raw = string; 10 | } 11 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/errors/movie_does_not_exist_error.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from ....core.exceptions.fern_http_exception import FernHTTPException 4 | from ..types.movie_id import MovieId 5 | 6 | 7 | class MovieDoesNotExistError(FernHTTPException): 8 | def __init__(self, error: MovieId): 9 | super().__init__(status_code=404, name="MovieDoesNotExistError", content=error) 10 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/addQuestionMarksToNullableProperties.d.ts: -------------------------------------------------------------------------------- 1 | export declare type addQuestionMarksToNullableProperties = { 2 | [K in OptionalKeys]?: T[K]; 3 | } & Pick>; 4 | export declare type OptionalKeys = { 5 | [K in keyof T]-?: undefined extends T[K] ? K : null extends T[K] ? K : 1 extends (any extends T[K] ? 0 : 1) ? never : K; 6 | }[keyof T]; 7 | export declare type RequiredKeys = Exclude>; 8 | -------------------------------------------------------------------------------- /frontend/src/api/generated/Client.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import { Imdb } from "./api/resources/imdb/client/Client"; 5 | export declare namespace ImdbApiClient { 6 | interface Options { 7 | environment: string; 8 | } 9 | } 10 | export declare class ImdbApiClient { 11 | private readonly options; 12 | constructor(options: ImdbApiClient.Options); 13 | private _imdb; 14 | get imdb(): Imdb; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/types.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema, Schema } from "../../Schema"; 2 | export declare type ObjectLikeSchema = Schema & BaseSchema & ObjectLikeUtils; 3 | export interface ObjectLikeUtils { 4 | withParsedProperties: >(properties: { 5 | [K in keyof T]: T[K] | ((parsed: Parsed) => T[K]); 6 | }) => ObjectLikeSchema; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/discriminant.d.ts: -------------------------------------------------------------------------------- 1 | export declare function discriminant(parsedDiscriminant: ParsedDiscriminant, rawDiscriminant: RawDiscriminant): Discriminant; 2 | export interface Discriminant { 3 | parsedDiscriminant: ParsedDiscriminant; 4 | rawDiscriminant: RawDiscriminant; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/Schema.js: -------------------------------------------------------------------------------- 1 | export const SchemaType = { 2 | DATE: "date", 3 | ENUM: "enum", 4 | LIST: "list", 5 | STRING_LITERAL: "stringLiteral", 6 | OBJECT: "object", 7 | ANY: "any", 8 | BOOLEAN: "boolean", 9 | NUMBER: "number", 10 | STRING: "string", 11 | UNKNOWN: "unknown", 12 | RECORD: "record", 13 | SET: "set", 14 | UNION: "union", 15 | UNDISCRIMINATED_UNION: "undiscriminatedUnion", 16 | OPTIONAL: "optional", 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/createIdentitySchemaCreator.js: -------------------------------------------------------------------------------- 1 | import { getSchemaUtils } from "../builders/schema-utils"; 2 | export function createIdentitySchemaCreator(schemaType, validate) { 3 | return () => { 4 | const baseSchema = { 5 | parse: validate, 6 | json: validate, 7 | getType: () => schemaType, 8 | }; 9 | return { 10 | ...baseSchema, 11 | ...getSchemaUtils(baseSchema), 12 | }; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /frontend/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fern starter 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/index.d.ts: -------------------------------------------------------------------------------- 1 | export { getObjectUtils, object } from "./object"; 2 | export { isProperty, property, type Property } from "./property"; 3 | export { type BaseObjectSchema, type inferObjectSchemaFromPropertySchemas, type inferParsedObject, type inferParsedObjectFromPropertySchemas, type inferParsedPropertySchema, type inferRawKey, type inferRawObject, type inferRawObjectFromPropertySchemas, type inferRawPropertySchema, type ObjectSchema, type ObjectUtils, type PropertySchemas, } from "./types"; 4 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/errors/MovieDoesNotExistError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as errors from "../../../../errors"; 5 | export class MovieDoesNotExistError extends errors.ImdbApiError { 6 | constructor(body) { 7 | super({ 8 | message: "MovieDoesNotExistError", 9 | statusCode: 404, 10 | body: body, 11 | }); 12 | Object.setPrototypeOf(this, MovieDoesNotExistError.prototype); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .fern_http_exception import FernHTTPException 4 | from .handlers import default_exception_handler, fern_http_exception_handler, http_exception_handler 5 | from .unauthorized import UnauthorizedException 6 | 7 | __all__ = [ 8 | "FernHTTPException", 9 | "UnauthorizedException", 10 | "default_exception_handler", 11 | "fern_http_exception_handler", 12 | "http_exception_handler", 13 | ] 14 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/exceptions/unauthorized.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import typing 4 | 5 | from .fern_http_exception import FernHTTPException 6 | 7 | 8 | class UnauthorizedException(FernHTTPException): 9 | """ 10 | This is the exception that is thrown by Fern when auth is not present on a 11 | request. 12 | """ 13 | 14 | def __init__(self, content: typing.Optional[str] = None) -> None: 15 | super().__init__(status_code=401, content=content) 16 | -------------------------------------------------------------------------------- /frontend/src/api/generated/errors/ImdbApiError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | export class ImdbApiError extends Error { 5 | statusCode; 6 | body; 7 | constructor({ message, statusCode, body }) { 8 | super(message); 9 | Object.setPrototypeOf(this, ImdbApiError.prototype); 10 | if (statusCode != null) { 11 | this.statusCode = statusCode; 12 | } 13 | if (body !== undefined) { 14 | this.body = body; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/client/Client.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import { ImdbApi } from "../../../.."; 5 | export declare namespace Imdb { 6 | interface Options { 7 | environment: string; 8 | } 9 | } 10 | export declare class Imdb { 11 | private readonly options; 12 | constructor(options: Imdb.Options); 13 | /** 14 | * @throws {ImdbApi.MovieDoesNotExistError} 15 | */ 16 | getMovie(movieId: ImdbApi.MovieId): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/types.d.ts: -------------------------------------------------------------------------------- 1 | import { inferParsed, inferRaw, Schema } from "../../Schema"; 2 | export declare type UndiscriminatedUnionSchema = Schema, inferParsedUnidiscriminatedUnionSchema>; 3 | export declare type inferRawUnidiscriminatedUnionSchema = inferRaw; 4 | export declare type inferParsedUnidiscriminatedUnionSchema = inferParsed; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/serialization/resources/imdb/types/Movie.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import * as serializers from "../../.."; 5 | import { ImdbApi } from "../../../.."; 6 | import * as core from "../../../../core"; 7 | export declare const Movie: core.serialization.ObjectSchema; 8 | export declare namespace Movie { 9 | interface Raw { 10 | id: serializers.MovieId.Raw; 11 | title: string; 12 | rating: number; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/fern.yml: -------------------------------------------------------------------------------- 1 | name: "Fern" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | fern-check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Install fern 15 | run: npm install -g fern-api 16 | - name: fern check 17 | run: fern check 18 | - name: Verify generated code is up to date 19 | run: | 20 | fern generate 21 | git --no-pager diff --exit-code 22 | env: 23 | FERN_TOKEN: dummy -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/property.d.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../Schema"; 2 | export declare function property(rawKey: RawKey, valueSchema: Schema): Property; 3 | export interface Property { 4 | rawKey: RawKey; 5 | valueSchema: Schema; 6 | isProperty: true; 7 | } 8 | export declare function isProperty>(maybeProperty: unknown): maybeProperty is O; 9 | -------------------------------------------------------------------------------- /.github/workflows/frontend.yml: -------------------------------------------------------------------------------- 1 | name: "Frontend" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: ./frontend 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Install dependencies 18 | run: yarn install 19 | - name: Lint 20 | run: yarn lint 21 | - name: Check formatting 22 | run: yarn format:check 23 | - name: Build frontend 24 | run: yarn build -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/lazy.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema, Schema } from "../../Schema"; 2 | export declare type SchemaGetter> = () => SchemaType | Promise; 3 | export declare function lazy(getter: SchemaGetter>): Schema; 4 | export declare function constructLazyBaseSchema(getter: SchemaGetter>): BaseSchema; 5 | export declare function getMemoizedSchema>(getter: SchemaGetter): Promise; 6 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/types.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema } from "../../Schema"; 2 | import { SchemaUtils } from "../schema-utils"; 3 | export declare type RecordSchema = BaseRecordSchema & SchemaUtils, Record>; 4 | export declare type BaseRecordSchema = BaseSchema, Record>; 5 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/utils/isPlainObject.js: -------------------------------------------------------------------------------- 1 | export const NOT_AN_OBJECT_ERROR_MESSAGE = "Not an object"; 2 | // borrowed from https://github.com/lodash/lodash/blob/master/isPlainObject.js 3 | export function isPlainObject(value) { 4 | if (typeof value !== "object" || value === null) { 5 | return false; 6 | } 7 | if (Object.getPrototypeOf(value) === null) { 8 | return true; 9 | } 10 | let proto = value; 11 | while (Object.getPrototypeOf(proto) !== null) { 12 | proto = Object.getPrototypeOf(proto); 13 | } 14 | return Object.getPrototypeOf(value) === proto; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/number.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export const number = createIdentitySchemaCreator(SchemaType.NUMBER, (value) => { 4 | if (typeof value === "number") { 5 | return { 6 | ok: true, 7 | value, 8 | }; 9 | } 10 | else { 11 | return { 12 | ok: false, 13 | errors: [ 14 | { 15 | path: [], 16 | message: "Not a number", 17 | }, 18 | ], 19 | }; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/string.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export const string = createIdentitySchemaCreator(SchemaType.STRING, (value) => { 4 | if (typeof value === "string") { 5 | return { 6 | ok: true, 7 | value, 8 | }; 9 | } 10 | else { 11 | return { 12 | ok: false, 13 | errors: [ 14 | { 15 | path: [], 16 | message: "Not a string", 17 | }, 18 | ], 19 | }; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /fern/api/generators.yml: -------------------------------------------------------------------------------- 1 | default-group: local 2 | groups: 3 | local: 4 | generators: 5 | - name: fernapi/fern-fastapi-server 6 | version: 0.1.0 7 | output: 8 | location: local-file-system 9 | path: ../../backend/src/fern_fastapi_starter/api/generated 10 | - name: fernapi/fern-typescript-sdk 11 | version: 0.5.2 12 | output: 13 | location: local-file-system 14 | path: ../../frontend/src/api/generated 15 | config: 16 | outputEsm: true 17 | - name: fernapi/fern-openapi 18 | version: 0.0.24 19 | output: 20 | location: local-file-system 21 | path: ../../openapi 22 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/primitives/boolean.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export const boolean = createIdentitySchemaCreator(SchemaType.BOOLEAN, (value) => { 4 | if (typeof value === "boolean") { 5 | return { 6 | ok: true, 7 | value, 8 | }; 9 | } 10 | else { 11 | return { 12 | ok: false, 13 | errors: [ 14 | { 15 | path: [], 16 | message: "Not a boolean", 17 | }, 18 | ], 19 | }; 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/__init__.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from .datetime_utils import serialize_datetime 4 | from .exceptions import ( 5 | FernHTTPException, 6 | UnauthorizedException, 7 | default_exception_handler, 8 | fern_http_exception_handler, 9 | http_exception_handler, 10 | ) 11 | from .route_args import route_args 12 | from .security import BearerToken 13 | 14 | __all__ = [ 15 | "BearerToken", 16 | "FernHTTPException", 17 | "UnauthorizedException", 18 | "default_exception_handler", 19 | "fern_http_exception_handler", 20 | "http_exception_handler", 21 | "route_args", 22 | "serialize_datetime", 23 | ] 24 | -------------------------------------------------------------------------------- /fern/api/definition/imdb.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json 2 | 3 | service: 4 | auth: false 5 | base-path: /movies 6 | endpoints: 7 | getMovie: 8 | method: GET 9 | path: /{movieId} 10 | path-parameters: 11 | movieId: MovieId 12 | response: Movie 13 | errors: 14 | - MovieDoesNotExistError 15 | 16 | types: 17 | MovieId: string 18 | 19 | Movie: 20 | properties: 21 | id: MovieId 22 | title: string 23 | rating: 24 | type: double 25 | docs: The rating scale is one to five stars 26 | 27 | errors: 28 | MovieDoesNotExistError: 29 | status-code: 404 30 | type: MovieId 31 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/getObjectLikeUtils.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema } from "../../Schema"; 2 | import { ObjectLikeSchema, ObjectLikeUtils } from "./types"; 3 | export declare function getObjectLikeUtils(schema: BaseSchema): ObjectLikeUtils; 4 | /** 5 | * object-like utils are defined in one file to resolve issues with circular imports 6 | */ 7 | export declare function withParsedProperties(objectLike: BaseSchema, properties: { 8 | [K in keyof Properties]: Properties[K] | ((parsed: ParsedObjectShape) => Properties[K]); 9 | }): ObjectLikeSchema; 10 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fern_fastapi_starter" 3 | version = "0.0.0" 4 | description = "" 5 | authors = [] 6 | 7 | [tool.poetry.scripts] 8 | start = "src.fern_fastapi_starter.server:start" 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.8" 12 | fastapi = "^0.79.0" 13 | fastapi-utils = "^0.2.1" 14 | uvicorn = "^0.18.2" 15 | pydantic = "^1.9.2" 16 | pre-commit = "^2.20.0" 17 | 18 | [tool.poetry.dev-dependencies] 19 | mypy = "^0.971" 20 | black = "^22.6.0" 21 | flake8 = "^5.0.4" 22 | sqlalchemy2-stubs = "^0.0.2-alpha.26" 23 | 24 | [tool.black] 25 | line-length = 120 26 | target-version = ['py38'] 27 | include = '\.pyi?$' 28 | 29 | [build-system] 30 | requires = ["poetry-core>=1.0.0"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/server.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi.middleware.cors import CORSMiddleware 4 | 5 | from .api.generated.register import register 6 | from .movies_service import MoviesService 7 | 8 | app = FastAPI() 9 | 10 | register(app, imdb=MoviesService()) 11 | 12 | app.add_middleware( 13 | CORSMiddleware, 14 | allow_origins=["http://localhost:5173"], 15 | allow_credentials=True, 16 | allow_methods=["*"], 17 | allow_headers=["*"], 18 | ) 19 | 20 | 21 | def start() -> None: 22 | uvicorn.run( 23 | "src.fern_fastapi_starter.server:app", 24 | host="0.0.0.0", 25 | port=8080, 26 | reload=True, 27 | ) 28 | 29 | 30 | if __name__ == "__main__": 31 | start() 32 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | workflow_dispatch: 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | stale-pr-message: 'This PR is stale because it has been open 25 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 17 | close-pr-message: 'This PR was closed because it has been inactive for 5 days after being marked stale.' 18 | days-before-issue-stale: -1 19 | days-before-issue-close: -1 20 | days-before-pr-stale: 25 21 | days-before-pr-close: 5 22 | operations-per-run: 300 23 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/lazyObject.js: -------------------------------------------------------------------------------- 1 | import { getObjectUtils } from "../object"; 2 | import { getObjectLikeUtils } from "../object-like"; 3 | import { getSchemaUtils } from "../schema-utils"; 4 | import { constructLazyBaseSchema, getMemoizedSchema } from "./lazy"; 5 | export function lazyObject(getter) { 6 | const baseSchema = { 7 | ...constructLazyBaseSchema(getter), 8 | _getRawProperties: async () => (await getMemoizedSchema(getter))._getRawProperties(), 9 | _getParsedProperties: async () => (await getMemoizedSchema(getter))._getParsedProperties(), 10 | }; 11 | return { 12 | ...baseSchema, 13 | ...getSchemaUtils(baseSchema), 14 | ...getObjectLikeUtils(baseSchema), 15 | ...getObjectUtils(baseSchema), 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | plugins: ["@typescript-eslint", "react"], 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/strict", 11 | "plugin:@typescript-eslint/eslint-recommended", 12 | "plugin:react/recommended", 13 | "plugin:react-hooks/recommended", 14 | ], 15 | settings: { 16 | react: { 17 | version: "detect", 18 | }, 19 | }, 20 | overrides: [], 21 | ignorePatterns: ["*.js", "*.cjs"], 22 | parser: "@typescript-eslint/parser", 23 | parserOptions: { 24 | ecmaFeatures: { 25 | jsx: true, 26 | }, 27 | ecmaVersion: "latest", 28 | sourceType: "module", 29 | project: "./tsconfig.json", 30 | }, 31 | rules: { 32 | "react/react-in-jsx-scope": "off", 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v1 16 | with: 17 | github-token: "${{ secrets.GITHUB_TOKEN }}" 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{contains(steps.metadata.outputs.dependency-names, 'my-dependency') && steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/literals/stringLiteral.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export function stringLiteral(literal) { 4 | const schemaCreator = createIdentitySchemaCreator(SchemaType.STRING_LITERAL, (value) => { 5 | if (value === literal) { 6 | return { 7 | ok: true, 8 | value: literal, 9 | }; 10 | } 11 | else { 12 | return { 13 | ok: false, 14 | errors: [ 15 | { 16 | path: [], 17 | message: `Not equal to "${literal}"`, 18 | }, 19 | ], 20 | }; 21 | } 22 | }); 23 | return schemaCreator(); 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/security/bearer.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import fastapi 4 | 5 | from ..exceptions import UnauthorizedException 6 | 7 | 8 | class BearerToken: 9 | def __init__(self, token: str): 10 | self.token = token 11 | 12 | 13 | def HTTPBearer(request: fastapi.requests.Request) -> BearerToken: 14 | authorization_header_value = request.headers.get("Authorization") 15 | if not authorization_header_value: 16 | raise UnauthorizedException("Missing Authorization header") 17 | scheme, _, token = authorization_header_value.partition(" ") 18 | if scheme.lower() != "bearer": 19 | raise UnauthorizedException("Authorization header scheme is not bearer") 20 | if not token: 21 | raise UnauthorizedException("Authorization header is missing a token") 22 | return BearerToken(token) 23 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/lazy/lazy.js: -------------------------------------------------------------------------------- 1 | import { getSchemaUtils } from "../schema-utils"; 2 | export function lazy(getter) { 3 | const baseSchema = constructLazyBaseSchema(getter); 4 | return { 5 | ...baseSchema, 6 | ...getSchemaUtils(baseSchema), 7 | }; 8 | } 9 | export function constructLazyBaseSchema(getter) { 10 | return { 11 | parse: async (raw, opts) => (await getMemoizedSchema(getter)).parse(raw, opts), 12 | json: async (parsed, opts) => (await getMemoizedSchema(getter)).json(parsed, opts), 13 | getType: async () => (await getMemoizedSchema(getter)).getType(), 14 | }; 15 | } 16 | export async function getMemoizedSchema(getter) { 17 | const castedGetter = getter; 18 | if (castedGetter.__zurg_memoized == null) { 19 | castedGetter.__zurg_memoized = await getter(); 20 | } 21 | return castedGetter.__zurg_memoized; 22 | } 23 | -------------------------------------------------------------------------------- /frontend/dist/assets/index-938200cc.css: -------------------------------------------------------------------------------- 1 | #root{max-width:1280px;margin:0 auto;padding:2rem}.button-container{display:flex;justify-content:center}:root{font-family:Inter,Avenir,Helvetica,Arial,sans-serif;font-size:16px;line-height:24px;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%}body{margin:0;display:flex;min-width:320px;min-height:100vh}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}pre{background-color:#1a1a1a;padding:10px;border-radius:6px}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}} 2 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/enum/enum.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { createIdentitySchemaCreator } from "../../utils/createIdentitySchemaCreator"; 3 | export function enum_(values) { 4 | const validValues = new Set(values); 5 | const schemaCreator = createIdentitySchemaCreator(SchemaType.ENUM, (value, { allowUnrecognizedEnumValues } = {}) => { 6 | if (typeof value === "string" && (validValues.has(value) || allowUnrecognizedEnumValues)) { 7 | return { 8 | ok: true, 9 | value: value, 10 | }; 11 | } 12 | else { 13 | return { 14 | ok: false, 15 | errors: [ 16 | { 17 | path: [], 18 | message: "Not one of the allowed values", 19 | }, 20 | ], 21 | }; 22 | } 23 | }); 24 | return schemaCreator(); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | import "./App.css"; 3 | import { ImdbApi, ImdbApiClient } from "./api"; 4 | import { Movie } from "./api/generated/serialization"; 5 | 6 | const BACKEND_CLIENT = new ImdbApiClient({ 7 | environment: "http://localhost:8080", 8 | }); 9 | 10 | function App() { 11 | const [movie, setMovie] = useState(); 12 | 13 | const onClick = useCallback(async () => { 14 | try { 15 | const movie = await BACKEND_CLIENT.imdb.getMovie("goodwill-hunting"); 16 | setMovie(movie); 17 | } catch (e) { 18 | console.error("Failed to get movie", e); 19 | } 20 | }, []); 21 | 22 | return ( 23 |
24 |
25 | 26 |
27 | {movie != null &&
{JSON.stringify(movie, undefined, 4)}
} 28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imdb", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "format": "prettier --write src/**", 11 | "format:check": "prettier --check src/**", 12 | "lint": "eslint", 13 | "lint:fix": "eslint --fix" 14 | }, 15 | "dependencies": { 16 | "axios": "^1.2.5", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "url-join": "^5.0.0" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "^18.0.26", 23 | "@types/react-dom": "^18.0.9", 24 | "@types/url-join": "^4.0.1", 25 | "@typescript-eslint/eslint-plugin": "latest", 26 | "@typescript-eslint/parser": "latest", 27 | "@vitejs/plugin-react": "^3.0.0", 28 | "eslint": "^8.32.0", 29 | "eslint-plugin-react": "latest", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "prettier": "^2.8.3", 32 | "typescript": "4.6.4", 33 | "vite": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/types/movie.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import datetime as dt 4 | import typing 5 | 6 | import pydantic 7 | 8 | from ....core.datetime_utils import serialize_datetime 9 | from .movie_id import MovieId 10 | 11 | 12 | class Movie(pydantic.BaseModel): 13 | id: MovieId 14 | title: str 15 | rating: float = pydantic.Field(description=("The rating scale is one to five stars\n")) 16 | 17 | def json(self, **kwargs: typing.Any) -> str: 18 | kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} 19 | return super().json(**kwargs_with_defaults) 20 | 21 | def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: 22 | kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} 23 | return super().dict(**kwargs_with_defaults) 24 | 25 | class Config: 26 | frozen = True 27 | extra = pydantic.Extra.forbid 28 | json_encoders = {dt.datetime: serialize_datetime} 29 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/types.d.ts: -------------------------------------------------------------------------------- 1 | import { inferParsedObject, inferRawObject, ObjectSchema } from "../object"; 2 | import { Discriminant } from "./discriminant"; 3 | export declare type UnionSubtypes = { 4 | [K in DiscriminantValues]: ObjectSchema; 5 | }; 6 | export declare type inferRawUnion, U extends UnionSubtypes> = { 7 | [K in keyof U]: Record, K> & inferRawObject; 8 | }[keyof U]; 9 | export declare type inferParsedUnion, U extends UnionSubtypes> = { 10 | [K in keyof U]: Record, K> & inferParsedObject; 11 | }[keyof U]; 12 | export declare type inferRawDiscriminant> = D extends string ? D : D extends Discriminant ? Raw : never; 13 | export declare type inferParsedDiscriminant> = D extends string ? D : D extends Discriminant ? Parsed : never; 14 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/Fetcher.d.ts: -------------------------------------------------------------------------------- 1 | import { APIResponse } from "./APIResponse"; 2 | export declare type FetchFunction = (args: Fetcher.Args) => Promise>; 3 | export declare namespace Fetcher { 4 | interface Args { 5 | url: string; 6 | method: string; 7 | contentType?: string; 8 | headers?: Record; 9 | queryParameters?: URLSearchParams; 10 | body?: unknown; 11 | timeoutMs?: number; 12 | withCredentials?: boolean; 13 | } 14 | type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError; 15 | interface FailedStatusCodeError { 16 | reason: "status-code"; 17 | statusCode: number; 18 | body: unknown; 19 | } 20 | interface NonJsonError { 21 | reason: "non-json"; 22 | statusCode: number; 23 | rawBody: string; 24 | } 25 | interface TimeoutError { 26 | reason: "timeout"; 27 | } 28 | interface UnknownError { 29 | reason: "unknown"; 30 | errorMessage: string; 31 | } 32 | } 33 | export declare const fetcher: FetchFunction; 34 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/types/movie_id.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from __future__ import annotations 4 | 5 | import datetime as dt 6 | import typing 7 | 8 | import pydantic 9 | 10 | from ....core.datetime_utils import serialize_datetime 11 | 12 | 13 | class MovieId(pydantic.BaseModel): 14 | __root__: str 15 | 16 | def get_as_str(self) -> str: 17 | return self.__root__ 18 | 19 | @staticmethod 20 | def from_str(value: str) -> MovieId: 21 | return MovieId(__root__=value) 22 | 23 | def json(self, **kwargs: typing.Any) -> str: 24 | kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} 25 | return super().json(**kwargs_with_defaults) 26 | 27 | def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: 28 | kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} 29 | return super().dict(**kwargs_with_defaults) 30 | 31 | class Config: 32 | frozen = True 33 | extra = pydantic.Extra.forbid 34 | json_encoders = {dt.datetime: serialize_datetime} 35 | -------------------------------------------------------------------------------- /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | name: "Backend" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: ./backend 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - uses: actions/setup-python@v3 18 | - name: Install Poetry 19 | uses: snok/install-poetry@v1 20 | - name: Install dependencies 21 | run: poetry install 22 | - name: mypy 23 | run: poetry run mypy 24 | 25 | dockerize: 26 | runs-on: ubuntu-latest 27 | needs: build 28 | if: github.ref == 'refs/heads/main' 29 | defaults: 30 | run: 31 | working-directory: ./backend 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - uses: actions/setup-python@v3 36 | - name: Install Poetry 37 | uses: snok/install-poetry@v1 38 | - name: Install dependencies 39 | run: poetry install 40 | - name: Install dependencies 41 | run: poetry install 42 | - name: Docker build 43 | run: | 44 | docker build -f Dockerfile -t "server" . -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/datetime_utils.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import datetime as dt 4 | 5 | 6 | def serialize_datetime(v: dt.datetime) -> str: 7 | """ 8 | Serialize a datetime including timezone info. 9 | 10 | Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. 11 | 12 | UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. 13 | """ 14 | 15 | def _serialize_zoned_datetime(v: dt.datetime) -> str: 16 | if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): 17 | # UTC is a special case where we use "Z" at the end instead of "+00:00" 18 | return v.isoformat().replace("+00:00", "Z") 19 | else: 20 | # Delegate to the typical +/- offset format 21 | return v.isoformat() 22 | 23 | if v.tzinfo is not None: 24 | return _serialize_zoned_datetime(v) 25 | else: 26 | local_tz = dt.datetime.now().astimezone().tzinfo 27 | localized_dt = v.replace(tzinfo=local_tz) 28 | return _serialize_zoned_datetime(localized_dt) 29 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/getSchemaUtils.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema, Schema, SchemaOptions } from "../../Schema"; 2 | export interface SchemaUtils { 3 | optional: () => Schema; 4 | transform: (transformer: SchemaTransformer) => Schema; 5 | parseOrThrow: (raw: unknown, opts?: SchemaOptions) => Promise; 6 | jsonOrThrow: (raw: unknown, opts?: SchemaOptions) => Promise; 7 | } 8 | export interface SchemaTransformer { 9 | transform: (parsed: Parsed) => Transformed; 10 | untransform: (transformed: any) => Parsed; 11 | } 12 | export declare function getSchemaUtils(schema: BaseSchema): SchemaUtils; 13 | /** 14 | * schema utils are defined in one file to resolve issues with circular imports 15 | */ 16 | export declare function optional(schema: BaseSchema): Schema; 17 | export declare function transform(schema: BaseSchema, transformer: SchemaTransformer): Schema; 18 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/undiscriminated-union/undiscriminatedUnion.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { getSchemaUtils } from "../schema-utils"; 3 | export function undiscriminatedUnion(schemas) { 4 | const baseSchema = { 5 | parse: async (raw, opts) => { 6 | return validateAndTransformUndiscriminatedUnion((schema) => schema.parse(raw, opts), schemas); 7 | }, 8 | json: async (parsed, opts) => { 9 | return validateAndTransformUndiscriminatedUnion((schema) => schema.json(parsed, opts), schemas); 10 | }, 11 | getType: () => SchemaType.UNDISCRIMINATED_UNION, 12 | }; 13 | return { 14 | ...baseSchema, 15 | ...getSchemaUtils(baseSchema), 16 | }; 17 | } 18 | async function validateAndTransformUndiscriminatedUnion(transform, schemas) { 19 | const errors = []; 20 | for (const schema of schemas) { 21 | const transformed = await transform(schema); 22 | if (transformed.ok) { 23 | return transformed; 24 | } 25 | else if (errors.length === 0) { 26 | errors.push(...transformed.errors); 27 | } 28 | } 29 | return { 30 | ok: false, 31 | errors, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/set/set.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { list } from "../list"; 3 | import { getSchemaUtils } from "../schema-utils"; 4 | export function set(schema) { 5 | const listSchema = list(schema); 6 | const baseSchema = { 7 | parse: async (raw, opts) => { 8 | const parsedList = await listSchema.parse(raw, opts); 9 | if (parsedList.ok) { 10 | return { 11 | ok: true, 12 | value: new Set(parsedList.value), 13 | }; 14 | } 15 | else { 16 | return parsedList; 17 | } 18 | }, 19 | json: async (parsed, opts) => { 20 | if (!(parsed instanceof Set)) { 21 | return { 22 | ok: false, 23 | errors: [ 24 | { 25 | path: [], 26 | message: "Not a Set", 27 | }, 28 | ], 29 | }; 30 | } 31 | const jsonList = await listSchema.json([...parsed], opts); 32 | return jsonList; 33 | }, 34 | getType: () => SchemaType.SET, 35 | }; 36 | return { 37 | ...baseSchema, 38 | ...getSchemaUtils(baseSchema), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/exceptions/fern_http_exception.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | from __future__ import annotations 4 | 5 | import abc 6 | import http 7 | import typing 8 | import uuid 9 | 10 | import fastapi 11 | import pydantic 12 | 13 | 14 | class FernHTTPException(abc.ABC, fastapi.HTTPException): 15 | def __init__( 16 | self, status_code: int, name: typing.Optional[str] = None, content: typing.Optional[typing.Any] = None 17 | ): 18 | super().__init__(status_code=status_code) 19 | self.name = name 20 | self.status_code = status_code 21 | self.content = content 22 | 23 | class Body(pydantic.BaseModel): 24 | error: typing.Optional[str] 25 | error_instance_id: uuid.UUID = pydantic.Field(alias="errorInstanceId", default_factory=uuid.uuid4) 26 | content: typing.Optional[typing.Any] 27 | 28 | class Config: 29 | frozen = True 30 | allow_population_by_field_name = True 31 | 32 | def to_json_response(self) -> fastapi.responses.JSONResponse: 33 | body = FernHTTPException.Body(error=self.name, content=self.content or http.HTTPStatus(self.status_code).phrase) 34 | content = fastapi.encoders.jsonable_encoder(body, exclude_none=True) 35 | return fastapi.responses.JSONResponse(content=content, status_code=self.status_code) 36 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/exceptions/handlers.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import logging 4 | 5 | import fastapi 6 | import starlette 7 | 8 | from .fern_http_exception import FernHTTPException 9 | 10 | 11 | def fern_http_exception_handler( 12 | request: fastapi.requests.Request, exc: FernHTTPException, skip_log: bool = False 13 | ) -> fastapi.responses.JSONResponse: 14 | if not skip_log: 15 | logging.getLogger(__name__).error(f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc) 16 | return exc.to_json_response() 17 | 18 | 19 | def http_exception_handler( 20 | request: fastapi.requests.Request, exc: starlette.exceptions.HTTPException, skip_log: bool = False 21 | ) -> fastapi.responses.JSONResponse: 22 | if not skip_log: 23 | logging.getLogger(__name__).error(f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc) 24 | return FernHTTPException(status_code=exc.status_code, content=exc.detail).to_json_response() 25 | 26 | 27 | def default_exception_handler( 28 | request: fastapi.requests.Request, exc: Exception, skip_log: bool = False 29 | ) -> fastapi.responses.JSONResponse: 30 | if not skip_log: 31 | logging.getLogger(__name__).error(f"{exc.__class__.__name__} in {request.url.path}", exc_info=exc) 32 | return FernHTTPException(status_code=500, content="Internal Server Error").to_json_response() 33 | -------------------------------------------------------------------------------- /frontend/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | display: flex; 21 | min-width: 320px; 22 | min-height: 100vh; 23 | } 24 | 25 | button { 26 | border-radius: 8px; 27 | border: 1px solid transparent; 28 | padding: 0.6em 1.2em; 29 | font-size: 1em; 30 | font-weight: 500; 31 | font-family: inherit; 32 | background-color: #1a1a1a; 33 | cursor: pointer; 34 | transition: border-color 0.25s; 35 | } 36 | button:hover { 37 | border-color: #646cff; 38 | } 39 | button:focus, 40 | button:focus-visible { 41 | outline: 4px auto -webkit-focus-ring-color; 42 | } 43 | 44 | pre { 45 | background-color: #1a1a1a; 46 | padding: 10px; 47 | border-radius: 6px; 48 | } 49 | 50 | @media (prefers-color-scheme: light) { 51 | :root { 52 | color: #213547; 53 | background-color: #ffffff; 54 | } 55 | a:hover { 56 | color: #747bff; 57 | } 58 | button { 59 | background-color: #f9f9f9; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /openapi/openapi.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: api 4 | version: '' 5 | paths: 6 | /movies/{movieId}: 7 | get: 8 | operationId: imdb_getMovie 9 | tags: 10 | - Imdb 11 | parameters: 12 | - name: movieId 13 | in: path 14 | required: true 15 | schema: 16 | $ref: '#/components/schemas/MovieId' 17 | responses: 18 | '200': 19 | description: '' 20 | content: 21 | application/json: 22 | schema: 23 | $ref: '#/components/schemas/Movie' 24 | '404': 25 | description: '' 26 | content: 27 | application/json: 28 | schema: 29 | oneOf: 30 | - type: object 31 | properties: 32 | error: 33 | type: string 34 | enum: 35 | - MovieDoesNotExistError 36 | content: 37 | $ref: '#/components/schemas/MovieId' 38 | components: 39 | schemas: 40 | MovieId: 41 | title: MovieId 42 | type: string 43 | Movie: 44 | title: Movie 45 | type: object 46 | properties: 47 | id: 48 | $ref: '#/components/schemas/MovieId' 49 | title: 50 | type: string 51 | rating: 52 | type: number 53 | format: double 54 | description: The rating scale is one to five stars 55 | required: 56 | - id 57 | - title 58 | - rating 59 | securitySchemes: {} 60 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/register.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import glob 4 | import importlib 5 | import os 6 | import types 7 | 8 | import fastapi 9 | import starlette 10 | 11 | from .core.abstract_fern_service import AbstractFernService 12 | from .core.exceptions import default_exception_handler, fern_http_exception_handler, http_exception_handler 13 | from .core.exceptions.fern_http_exception import FernHTTPException 14 | from .resources.imdb.service.service import AbstractImdbService 15 | 16 | 17 | def register(_app: fastapi.FastAPI, *, imdb: AbstractImdbService) -> None: 18 | _app.include_router(__register_service(imdb)) 19 | 20 | _app.add_exception_handler(FernHTTPException, fern_http_exception_handler) 21 | _app.add_exception_handler(starlette.exceptions.HTTPException, http_exception_handler) 22 | _app.add_exception_handler(Exception, default_exception_handler) 23 | 24 | 25 | def __register_service(service: AbstractFernService) -> fastapi.APIRouter: 26 | router = fastapi.APIRouter() 27 | type(service)._init_fern(router) 28 | return router 29 | 30 | 31 | def register_validators(module: types.ModuleType) -> None: 32 | validators_directory: str = os.path.dirname(module.__file__) # type: ignore 33 | for path in glob.glob(os.path.join(validators_directory, "**/*.py"), recursive=True): 34 | if os.path.isfile(path): 35 | relative_path = os.path.relpath(path, start=validators_directory) 36 | module_path = ".".join([module.__name__] + relative_path[:-3].split("/")) 37 | importlib.import_module(module_path) 38 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/list/list.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { getSchemaUtils } from "../schema-utils"; 3 | export function list(schema) { 4 | const baseSchema = { 5 | parse: async (raw, opts) => validateAndTransformArray(raw, (item) => schema.parse(item, opts)), 6 | json: (parsed, opts) => validateAndTransformArray(parsed, (item) => schema.json(item, opts)), 7 | getType: () => SchemaType.LIST, 8 | }; 9 | return { 10 | ...baseSchema, 11 | ...getSchemaUtils(baseSchema), 12 | }; 13 | } 14 | async function validateAndTransformArray(value, transformItem) { 15 | if (!Array.isArray(value)) { 16 | return { 17 | ok: false, 18 | errors: [ 19 | { 20 | message: "Not a list", 21 | path: [], 22 | }, 23 | ], 24 | }; 25 | } 26 | const maybeValidItems = await Promise.all(value.map((item) => transformItem(item))); 27 | return maybeValidItems.reduce((acc, item, index) => { 28 | if (acc.ok && item.ok) { 29 | return { 30 | ok: true, 31 | value: [...acc.value, item.value], 32 | }; 33 | } 34 | const errors = []; 35 | if (!acc.ok) { 36 | errors.push(...acc.errors); 37 | } 38 | if (!item.ok) { 39 | errors.push(...item.errors.map((error) => ({ 40 | path: [`[${index}]`, ...error.path], 41 | message: error.message, 42 | }))); 43 | } 44 | return { 45 | ok: false, 46 | errors, 47 | }; 48 | }, { ok: true, value: [] }); 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/core/route_args.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import enum 4 | import inspect 5 | import typing 6 | 7 | import typing_extensions 8 | 9 | T = typing.TypeVar("T", bound=typing.Callable[..., typing.Any]) 10 | 11 | FERN_CONFIG_KEY = "__fern" 12 | 13 | 14 | class RouteArgs(typing_extensions.TypedDict): 15 | openapi_extra: typing.Optional[typing.Dict[str, typing.Any]] 16 | tags: typing.Optional[typing.List[typing.Union[str, enum.Enum]]] 17 | 18 | 19 | DEFAULT_ROUTE_ARGS = RouteArgs(openapi_extra=None, tags=None) 20 | 21 | 22 | def get_route_args(endpoint_function: typing.Callable[..., typing.Any], *, default_tag: str) -> RouteArgs: 23 | unwrapped = inspect.unwrap(endpoint_function, stop=(lambda f: hasattr(f, FERN_CONFIG_KEY))) 24 | route_args = typing.cast(RouteArgs, getattr(unwrapped, FERN_CONFIG_KEY, DEFAULT_ROUTE_ARGS)) 25 | if route_args["tags"] is None: 26 | return RouteArgs(openapi_extra=route_args["openapi_extra"], tags=[default_tag]) 27 | return route_args 28 | 29 | 30 | def route_args( 31 | openapi_extra: typing.Optional[typing.Dict[str, typing.Any]] = None, 32 | tags: typing.Optional[typing.List[typing.Union[str, enum.Enum]]] = None, 33 | ) -> typing.Callable[[T], T]: 34 | """ 35 | this decorator allows you to forward certain args to the FastAPI route decorator. 36 | 37 | usage: 38 | @route_args(openapi_extra=...) 39 | def your_endpoint_method(... 40 | 41 | currently supported args: 42 | - openapi_extra 43 | - tags 44 | 45 | if there's another FastAPI route arg you need to pass through, please 46 | contact the Fern team! 47 | """ 48 | 49 | def decorator(endpoint_function: T) -> T: 50 | setattr(endpoint_function, FERN_CONFIG_KEY, RouteArgs(openapi_extra=openapi_extra, tags=tags)) 51 | return endpoint_function 52 | 53 | return decorator 54 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/date/date.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { getSchemaUtils } from "../schema-utils"; 3 | // https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime 4 | const ISO_8601_REGEX = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; 5 | export function date() { 6 | const baseSchema = { 7 | parse: (raw) => { 8 | if (typeof raw === "string" && ISO_8601_REGEX.test(raw)) { 9 | return { 10 | ok: true, 11 | value: new Date(raw), 12 | }; 13 | } 14 | else { 15 | return { 16 | ok: false, 17 | errors: [ 18 | { 19 | path: [], 20 | message: "Not an ISO 8601 date string", 21 | }, 22 | ], 23 | }; 24 | } 25 | }, 26 | json: (date) => { 27 | if (date instanceof Date) { 28 | return { 29 | ok: true, 30 | value: date.toISOString(), 31 | }; 32 | } 33 | else { 34 | return { 35 | ok: false, 36 | errors: [ 37 | { 38 | path: [], 39 | message: "Not a Date object", 40 | }, 41 | ], 42 | }; 43 | } 44 | }, 45 | getType: () => SchemaType.DATE, 46 | }; 47 | return { 48 | ...baseSchema, 49 | ...getSchemaUtils(baseSchema), 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/Schema.d.ts: -------------------------------------------------------------------------------- 1 | import { SchemaUtils } from "./builders"; 2 | import { MaybePromise } from "./utils/MaybePromise"; 3 | export declare type Schema = BaseSchema & SchemaUtils; 4 | export declare type inferRaw = S extends Schema ? Raw : never; 5 | export declare type inferParsed = S extends Schema ? Parsed : never; 6 | export interface BaseSchema { 7 | parse: (raw: unknown, opts?: SchemaOptions) => MaybePromise>; 8 | json: (parsed: unknown, opts?: SchemaOptions) => MaybePromise>; 9 | getType: () => SchemaType | Promise; 10 | } 11 | export declare const SchemaType: { 12 | readonly DATE: "date"; 13 | readonly ENUM: "enum"; 14 | readonly LIST: "list"; 15 | readonly STRING_LITERAL: "stringLiteral"; 16 | readonly OBJECT: "object"; 17 | readonly ANY: "any"; 18 | readonly BOOLEAN: "boolean"; 19 | readonly NUMBER: "number"; 20 | readonly STRING: "string"; 21 | readonly UNKNOWN: "unknown"; 22 | readonly RECORD: "record"; 23 | readonly SET: "set"; 24 | readonly UNION: "union"; 25 | readonly UNDISCRIMINATED_UNION: "undiscriminatedUnion"; 26 | readonly OPTIONAL: "optional"; 27 | }; 28 | export declare type SchemaType = typeof SchemaType[keyof typeof SchemaType]; 29 | export declare type MaybeValid = Valid | Invalid; 30 | export interface Valid { 31 | ok: true; 32 | value: T; 33 | } 34 | export interface Invalid { 35 | ok: false; 36 | errors: ValidationError[]; 37 | } 38 | export interface ValidationError { 39 | path: string[]; 40 | message: string; 41 | } 42 | export interface SchemaOptions { 43 | /** 44 | * how to handle unrecognized keys in objects 45 | * 46 | * @default "fail" 47 | */ 48 | unrecognizedObjectKeys?: "fail" | "passthrough" | "strip"; 49 | /** 50 | * whether to fail when an unrecognized discriminant value is 51 | * encountered in a union 52 | * 53 | * @default false 54 | */ 55 | allowUnrecognizedUnionMembers?: boolean; 56 | /** 57 | * whether to fail when an unrecognized enum value is encountered 58 | * 59 | * @default false 60 | */ 61 | allowUnrecognizedEnumValues?: boolean; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object-like/getObjectLikeUtils.js: -------------------------------------------------------------------------------- 1 | import { filterObject } from "../../utils/filterObject"; 2 | import { isPlainObject, NOT_AN_OBJECT_ERROR_MESSAGE } from "../../utils/isPlainObject"; 3 | import { getSchemaUtils } from "../schema-utils"; 4 | export function getObjectLikeUtils(schema) { 5 | return { 6 | withParsedProperties: (properties) => withParsedProperties(schema, properties), 7 | }; 8 | } 9 | /** 10 | * object-like utils are defined in one file to resolve issues with circular imports 11 | */ 12 | export function withParsedProperties(objectLike, properties) { 13 | const objectSchema = { 14 | parse: async (raw, opts) => { 15 | const parsedObject = await objectLike.parse(raw, opts); 16 | if (!parsedObject.ok) { 17 | return parsedObject; 18 | } 19 | const additionalProperties = Object.entries(properties).reduce((processed, [key, value]) => { 20 | return { 21 | ...processed, 22 | [key]: typeof value === "function" ? value(parsedObject.value) : value, 23 | }; 24 | }, {}); 25 | return { 26 | ok: true, 27 | value: { 28 | ...parsedObject.value, 29 | ...additionalProperties, 30 | }, 31 | }; 32 | }, 33 | json: (parsed, opts) => { 34 | if (!isPlainObject(parsed)) { 35 | return { 36 | ok: false, 37 | errors: [ 38 | { 39 | path: [], 40 | message: NOT_AN_OBJECT_ERROR_MESSAGE, 41 | }, 42 | ], 43 | }; 44 | } 45 | // strip out added properties 46 | const addedPropertyKeys = new Set(Object.keys(properties)); 47 | const parsedWithoutAddedProperties = filterObject(parsed, Object.keys(parsed).filter((key) => !addedPropertyKeys.has(key))); 48 | return objectLike.json(parsedWithoutAddedProperties, opts); 49 | }, 50 | getType: () => objectLike.getType(), 51 | }; 52 | return { 53 | ...objectSchema, 54 | ...getSchemaUtils(objectSchema), 55 | ...getObjectLikeUtils(objectSchema), 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/api/generated/api/resources/imdb/client/Client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was auto-generated by Fern from our API Definition. 3 | */ 4 | import { ImdbApi } from "../../../.."; 5 | import * as serializers from "../../../../serialization"; 6 | import urlJoin from "url-join"; 7 | import * as core from "../../../../core"; 8 | import * as errors from "../../../../errors"; 9 | export class Imdb { 10 | options; 11 | constructor(options) { 12 | this.options = options; 13 | } 14 | /** 15 | * @throws {ImdbApi.MovieDoesNotExistError} 16 | */ 17 | async getMovie(movieId) { 18 | const _response = await core.fetcher({ 19 | url: urlJoin(this.options.environment, `/movies/${await serializers.MovieId.jsonOrThrow(movieId)}`), 20 | method: "GET", 21 | contentType: "application/json", 22 | }); 23 | if (_response.ok) { 24 | return await serializers.Movie.parseOrThrow(_response.body, { 25 | unrecognizedObjectKeys: "passthrough", 26 | allowUnrecognizedUnionMembers: true, 27 | allowUnrecognizedEnumValues: true, 28 | }); 29 | } 30 | if (_response.error.reason === "status-code") { 31 | switch (_response.error.body?.["error"]) { 32 | case "MovieDoesNotExistError": 33 | throw new ImdbApi.MovieDoesNotExistError(await serializers.MovieId.parseOrThrow(_response.error.body, { 34 | unrecognizedObjectKeys: "passthrough", 35 | allowUnrecognizedUnionMembers: true, 36 | allowUnrecognizedEnumValues: true, 37 | })); 38 | default: 39 | throw new errors.ImdbApiError({ 40 | statusCode: _response.error.statusCode, 41 | body: _response.error.body, 42 | }); 43 | } 44 | } 45 | switch (_response.error.reason) { 46 | case "non-json": 47 | throw new errors.ImdbApiError({ 48 | statusCode: _response.error.statusCode, 49 | body: _response.error.rawBody, 50 | }); 51 | case "timeout": 52 | throw new errors.ImdbApiTimeoutError(); 53 | case "unknown": 54 | throw new errors.ImdbApiError({ 55 | message: _response.error.errorMessage, 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/fetcher/Fetcher.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export const fetcher = async (args) => { 3 | const headers = {}; 4 | if (args.contentType != null) { 5 | headers["Content-Type"] = args.contentType; 6 | } 7 | if (args.headers != null) { 8 | for (const [key, value] of Object.entries(args.headers)) { 9 | if (value != null) { 10 | headers[key] = value; 11 | } 12 | } 13 | } 14 | try { 15 | const response = await axios({ 16 | url: args.url, 17 | params: args.queryParameters, 18 | method: args.method, 19 | headers, 20 | data: args.body, 21 | validateStatus: () => true, 22 | transformResponse: (response) => response, 23 | timeout: args.timeoutMs ?? 60_000, 24 | transitional: { 25 | clarifyTimeoutError: true, 26 | }, 27 | withCredentials: args.withCredentials, 28 | }); 29 | let body; 30 | if (response.data != null && response.data.length > 0) { 31 | try { 32 | body = JSON.parse(response.data) ?? undefined; 33 | } 34 | catch { 35 | return { 36 | ok: false, 37 | error: { 38 | reason: "non-json", 39 | statusCode: response.status, 40 | rawBody: response.data, 41 | }, 42 | }; 43 | } 44 | } 45 | if (response.status >= 200 && response.status < 300) { 46 | return { 47 | ok: true, 48 | body, 49 | }; 50 | } 51 | else { 52 | return { 53 | ok: false, 54 | error: { 55 | reason: "status-code", 56 | statusCode: response.status, 57 | body, 58 | }, 59 | }; 60 | } 61 | } 62 | catch (error) { 63 | if (error.code === "ETIMEDOUT") { 64 | return { 65 | ok: false, 66 | error: { 67 | reason: "timeout", 68 | }, 69 | }; 70 | } 71 | return { 72 | ok: false, 73 | error: { 74 | reason: "unknown", 75 | errorMessage: error.message, 76 | }, 77 | }; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/schema-utils/getSchemaUtils.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { JsonError } from "./JsonError"; 3 | import { ParseError } from "./ParseError"; 4 | export function getSchemaUtils(schema) { 5 | return { 6 | optional: () => optional(schema), 7 | transform: (transformer) => transform(schema, transformer), 8 | parseOrThrow: async (raw, opts) => { 9 | const parsed = await schema.parse(raw, opts); 10 | if (parsed.ok) { 11 | return parsed.value; 12 | } 13 | throw new ParseError(parsed.errors); 14 | }, 15 | jsonOrThrow: async (parsed, opts) => { 16 | const raw = await schema.json(parsed, opts); 17 | if (raw.ok) { 18 | return raw.value; 19 | } 20 | throw new JsonError(raw.errors); 21 | }, 22 | }; 23 | } 24 | /** 25 | * schema utils are defined in one file to resolve issues with circular imports 26 | */ 27 | export function optional(schema) { 28 | const baseSchema = { 29 | parse: (raw, opts) => { 30 | if (raw == null) { 31 | return { 32 | ok: true, 33 | value: undefined, 34 | }; 35 | } 36 | return schema.parse(raw, opts); 37 | }, 38 | json: (parsed, opts) => { 39 | if (parsed == null) { 40 | return { 41 | ok: true, 42 | value: null, 43 | }; 44 | } 45 | return schema.json(parsed, opts); 46 | }, 47 | getType: () => SchemaType.OPTIONAL, 48 | }; 49 | return { 50 | ...baseSchema, 51 | ...getSchemaUtils(baseSchema), 52 | }; 53 | } 54 | export function transform(schema, transformer) { 55 | const baseSchema = { 56 | parse: async (raw, opts) => { 57 | const parsed = await schema.parse(raw, opts); 58 | if (!parsed.ok) { 59 | return parsed; 60 | } 61 | return { 62 | ok: true, 63 | value: transformer.transform(parsed.value), 64 | }; 65 | }, 66 | json: async (transformed, opts) => { 67 | const parsed = await transformer.untransform(transformed); 68 | return schema.json(parsed, opts); 69 | }, 70 | getType: () => schema.getType(), 71 | }; 72 | return { 73 | ...baseSchema, 74 | ...getSchemaUtils(baseSchema), 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/types.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSchema, inferParsed, inferRaw, Schema } from "../../Schema"; 2 | import { addQuestionMarksToNullableProperties } from "../../utils/addQuestionMarksToNullableProperties"; 3 | import { ObjectLikeUtils } from "../object-like"; 4 | import { SchemaUtils } from "../schema-utils"; 5 | import { Property } from "./property"; 6 | export declare type ObjectSchema = BaseObjectSchema & ObjectLikeUtils & ObjectUtils & SchemaUtils; 7 | export interface BaseObjectSchema extends BaseSchema { 8 | _getRawProperties: () => Promise<(keyof Raw)[]>; 9 | _getParsedProperties: () => Promise<(keyof Parsed)[]>; 10 | } 11 | export interface ObjectUtils { 12 | extend: (schemas: ObjectSchema) => ObjectSchema; 13 | } 14 | export declare type inferRawObject> = O extends ObjectSchema ? Raw : never; 15 | export declare type inferParsedObject> = O extends ObjectSchema ? Parsed : never; 16 | export declare type inferObjectSchemaFromPropertySchemas> = ObjectSchema, inferParsedObjectFromPropertySchemas>; 17 | export declare type inferRawObjectFromPropertySchemas> = addQuestionMarksToNullableProperties<{ 18 | [ParsedKey in keyof T as inferRawKey]: inferRawPropertySchema; 19 | }>; 20 | export declare type inferParsedObjectFromPropertySchemas> = addQuestionMarksToNullableProperties<{ 21 | [K in keyof T]: inferParsedPropertySchema; 22 | }>; 23 | export declare type PropertySchemas = Record | Schema>; 24 | export declare type inferRawPropertySchema

| Schema> = P extends Property ? Raw : P extends Schema ? inferRaw

: never; 25 | export declare type inferParsedPropertySchema

| Schema> = P extends Property ? Parsed : P extends Schema ? inferParsed

: never; 26 | export declare type inferRawKey | Schema> = P extends Property ? Raw : ParsedKey; 27 | -------------------------------------------------------------------------------- /backend/src/fern_fastapi_starter/api/generated/resources/imdb/service/service.py: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by Fern from our API Definition. 2 | 3 | import abc 4 | import functools 5 | import inspect 6 | import logging 7 | import typing 8 | 9 | import fastapi 10 | 11 | from ....core.abstract_fern_service import AbstractFernService 12 | from ....core.exceptions.fern_http_exception import FernHTTPException 13 | from ....core.route_args import get_route_args 14 | from ..errors.movie_does_not_exist_error import MovieDoesNotExistError 15 | from ..types.movie import Movie 16 | 17 | 18 | class AbstractImdbService(AbstractFernService): 19 | """ 20 | AbstractImdbService is an abstract class containing the methods that you should implement. 21 | 22 | Each method is associated with an API route, which will be registered 23 | with FastAPI when you register your implementation using Fern's register() 24 | function. 25 | """ 26 | 27 | @abc.abstractmethod 28 | def get_movie(self, *, movie_id: str) -> Movie: 29 | ... 30 | 31 | """ 32 | Below are internal methods used by Fern to register your implementation. 33 | You can ignore them. 34 | """ 35 | 36 | @classmethod 37 | def _init_fern(cls, router: fastapi.APIRouter) -> None: 38 | cls.__init_get_movie(router=router) 39 | 40 | @classmethod 41 | def __init_get_movie(cls, router: fastapi.APIRouter) -> None: 42 | endpoint_function = inspect.signature(cls.get_movie) 43 | new_parameters: typing.List[inspect.Parameter] = [] 44 | for index, (parameter_name, parameter) in enumerate(endpoint_function.parameters.items()): 45 | if index == 0: 46 | new_parameters.append(parameter.replace(default=fastapi.Depends(cls))) 47 | elif parameter_name == "movie_id": 48 | new_parameters.append(parameter.replace(default=fastapi.Path(...))) 49 | else: 50 | new_parameters.append(parameter) 51 | setattr(cls.get_movie, "__signature__", endpoint_function.replace(parameters=new_parameters)) 52 | 53 | @functools.wraps(cls.get_movie) 54 | def wrapper(*args: typing.Any, **kwargs: typing.Any) -> Movie: 55 | try: 56 | return cls.get_movie(*args, **kwargs) 57 | except MovieDoesNotExistError as e: 58 | raise e 59 | except FernHTTPException as e: 60 | logging.getLogger(f"{cls.__module__}.{cls.__name__}").warn( 61 | f"Endpoint 'get_movie' unexpectedly threw {e.__class__.__name__}. " 62 | + f"If this was intentional, please add {e.__class__.__name__} to " 63 | + "the endpoint's errors list in your Fern Definition." 64 | ) 65 | raise e 66 | 67 | # this is necessary for FastAPI to find forward-ref'ed type hints. 68 | # https://github.com/tiangolo/fastapi/pull/5077 69 | wrapper.__globals__.update(cls.get_movie.__globals__) 70 | 71 | router.get( 72 | path="/movies/{movie_id}", 73 | response_model=Movie, 74 | description=AbstractImdbService.get_movie.__doc__, 75 | **get_route_args(cls.get_movie, default_tag="imdb"), 76 | )(wrapper) 77 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/record/record.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { entries } from "../../utils/entries"; 3 | import { isPlainObject, NOT_AN_OBJECT_ERROR_MESSAGE } from "../../utils/isPlainObject"; 4 | import { getSchemaUtils } from "../schema-utils"; 5 | export function record(keySchema, valueSchema) { 6 | const baseSchema = { 7 | parse: async (raw, opts) => { 8 | return validateAndTransformRecord({ 9 | value: raw, 10 | isKeyNumeric: (await keySchema.getType()) === SchemaType.NUMBER, 11 | transformKey: (key) => keySchema.parse(key, opts), 12 | transformValue: (value) => valueSchema.parse(value, opts), 13 | }); 14 | }, 15 | json: async (parsed, opts) => { 16 | return validateAndTransformRecord({ 17 | value: parsed, 18 | isKeyNumeric: (await keySchema.getType()) === SchemaType.NUMBER, 19 | transformKey: (key) => keySchema.json(key, opts), 20 | transformValue: (value) => valueSchema.json(value, opts), 21 | }); 22 | }, 23 | getType: () => SchemaType.RECORD, 24 | }; 25 | return { 26 | ...baseSchema, 27 | ...getSchemaUtils(baseSchema), 28 | }; 29 | } 30 | async function validateAndTransformRecord({ value, isKeyNumeric, transformKey, transformValue, }) { 31 | if (!isPlainObject(value)) { 32 | return { 33 | ok: false, 34 | errors: [ 35 | { 36 | path: [], 37 | message: NOT_AN_OBJECT_ERROR_MESSAGE, 38 | }, 39 | ], 40 | }; 41 | } 42 | return entries(value).reduce(async (accPromise, [stringKey, value]) => { 43 | // skip nullish keys 44 | if (value == null) { 45 | return accPromise; 46 | } 47 | const acc = await accPromise; 48 | let key = stringKey; 49 | if (isKeyNumeric) { 50 | const numberKey = stringKey.length > 0 ? Number(stringKey) : NaN; 51 | if (!isNaN(numberKey)) { 52 | key = numberKey; 53 | } 54 | } 55 | const transformedKey = await transformKey(key); 56 | const transformedValue = await transformValue(value); 57 | if (acc.ok && transformedKey.ok && transformedValue.ok) { 58 | return { 59 | ok: true, 60 | value: { 61 | ...acc.value, 62 | [transformedKey.value]: transformedValue.value, 63 | }, 64 | }; 65 | } 66 | const errors = []; 67 | if (!acc.ok) { 68 | errors.push(...acc.errors); 69 | } 70 | if (!transformedKey.ok) { 71 | errors.push(...transformedKey.errors.map((error) => ({ 72 | path: [`${key} (key)`, ...error.path], 73 | message: error.message, 74 | }))); 75 | } 76 | if (!transformedValue.ok) { 77 | errors.push(...transformedValue.errors.map((error) => ({ 78 | path: [stringKey, ...error.path], 79 | message: error.message, 80 | }))); 81 | } 82 | return { 83 | ok: false, 84 | errors, 85 | }; 86 | }, Promise.resolve({ ok: true, value: {} })); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starter repo for FastAPI + React, using [Fern](https://github.com/fern-api/fern) 2 | 3 | **Fern is an open source format for defining REST APIs.** Fern makes it easy to: 4 | 5 | 1. Define your API 6 | 1. Implement your API on the backend 7 | 1. Consume your API on the frontend 8 | 9 | ## Quick start 10 | 11 | ### Step 1: Fork this repo 12 | 13 | ### Step 2: Clone your fork 14 | 15 | ### Step 3: Generate code 16 | 17 | Most of the code for the backend and frontend is auto-generated by Fern! 18 | 19 | In a terminal, run: 20 | 21 | ``` 22 | npm install -g fern-api 23 | fern generate 24 | ``` 25 | 26 | ### Step 4: Run the backend 27 | 28 | _Prerequisite: Install [poetry](https://python-poetry.org/docs/)._ 29 | 30 | Run: 31 | 32 | ``` 33 | cd backend 34 | poetry install 35 | poetry run start 36 | ``` 37 | 38 | ### Step 5: Run the frontend 39 | 40 | In a new terminal, run: 41 | 42 | ``` 43 | cd frontend 44 | yarn install 45 | yarn start 46 | ``` 47 | 48 | Open [http://localhost:5173](http://localhost:5173) to see the frontend! 49 | 50 | ![frontend](assets/frontend-demo.gif) 51 | 52 | ## Developing the backend 53 | 54 | The [FastAPI](https://fastapi.tiangolo.com/) backend lives in the 55 | [backend](backend/) directory. 56 | 57 | To get started, open a VSCode window for the root of this repo: 58 | 59 | ``` 60 | code . 61 | ``` 62 | 63 | Install the recommended MyPy extensions. This will give you type checking in the IDE. 64 | 65 | Install dependencies: 66 | 67 | ``` 68 | cd backend 69 | poetry install 70 | ``` 71 | 72 | Run the server: 73 | 74 | ``` 75 | poetry run start 76 | ``` 77 | 78 | You can edit the server code, and the server will automatically reload. 79 | 80 | ### Auto-generated server code 81 | 82 | Nearly all the FastAPI and Pydantic code is generated by Fern when you run `fern generate`. 83 | The generated code lives in [backend/src/fern_fastapi_starter/api/generated](backend/src/fern_fastapi_starter/api/generated). 84 | 85 | In addition to saving you time, the auto-generated code gives you compile-time safety 86 | that your API is implemented correctly. For example, if you forget to define the `getMovie` 87 | endpoint, you'll get a compile error: 88 | 89 | ![backend mypy error](./assets/backend-mypy-error.png) 90 | 91 | ## Developing the frontend 92 | 93 | The frontend is generated using [React](https://reactjs.org/), 94 | [TypeScript](https://www.typescriptlang.org/) and [Vite](https://vitejs.dev/). 95 | 96 | To get started, open a new VSCode window for the `frontend/` directory: 97 | 98 | ``` 99 | cd frontend 100 | code . 101 | ``` 102 | 103 | Install dependencies: 104 | 105 | ``` 106 | yarn install 107 | ``` 108 | 109 | Run the frontend: 110 | 111 | ``` 112 | yarn start 113 | ``` 114 | 115 | Open [http://localhost:5173](http://localhost:5173) to see the frontend! 116 | 117 | You can edit the frontend code, and Vite will automatically reload your changes. 118 | 119 | ### Auto-generated API client 120 | 121 | The API client is automatically generated by Fern when you run `fern generate`. 122 | The generated client lives in [frontend/src/api/generated](frontend/src/api/generated). You can see it used 123 | in [frontend/src/App.tsx](frontend/src/App.tsx), 124 | 125 | In addition to saving you time, the auto-generated code gives you compile-time safety 126 | and autocomplete: 127 | 128 | ![frontend autocomplete](./assets/frontend-autocomplete.png) 129 | 130 | ## Changing the API 131 | 132 | The API is defined using [Fern](https://www.buildwithfern.com/). The definition 133 | lives in the [fern/](fern/api/definition) directory. You can edit these YAML files 134 | to update the API. Check out the [docs](https://docs.buildwithfern.com/definition) to read more about defining APIs. 135 | 136 | Most of the server and frontend code in this repo is **automatically generated 137 | by Fern.** You can regenerate the code using the Fern CLI: 138 | 139 | ``` 140 | npm install -g fern 141 | fern generate 142 | ``` 143 | 144 | This will output newly generated code: 145 | 146 | - Backend: [backend/src/fern_fastapi_starter/api/generated](backend/src/fern_fastapi_starter/api/generated) 147 | - Frontend: [frontend/src/api/generated](frontend/src/api/generated) 148 | 149 | When you change your API, you'll get compile errors on the backend if you're not 150 | implementing the API correctly. 151 | 152 | You can always use the command line to run mypy as well: 153 | 154 | ``` 155 | poetry run mypy 156 | ``` 157 | 158 | ## Issues & contributing 159 | 160 | If you run into any problems while using this ticket, plesae [file an 161 | issue](https://github.com/fern-api/fastapi-starter/issues). Of course, PRs are 162 | welcome and encouraged! 163 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/union/union.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { isPlainObject, NOT_AN_OBJECT_ERROR_MESSAGE } from "../../utils/isPlainObject"; 3 | import { keys } from "../../utils/keys"; 4 | import { enum_ } from "../enum"; 5 | import { getObjectLikeUtils } from "../object-like"; 6 | import { getSchemaUtils } from "../schema-utils"; 7 | export function union(discriminant, union) { 8 | const rawDiscriminant = typeof discriminant === "string" ? discriminant : discriminant.rawDiscriminant; 9 | const parsedDiscriminant = typeof discriminant === "string" 10 | ? discriminant 11 | : discriminant.parsedDiscriminant; 12 | const discriminantValueSchema = enum_(keys(union)); 13 | const baseSchema = { 14 | parse: async (raw, opts) => { 15 | return transformAndValidateUnion({ 16 | value: raw, 17 | discriminant: rawDiscriminant, 18 | transformedDiscriminant: parsedDiscriminant, 19 | transformDiscriminantValue: (discriminantValue) => discriminantValueSchema.parse(discriminantValue, { 20 | allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, 21 | }), 22 | getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], 23 | allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, 24 | transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => additionalPropertiesSchema.parse(additionalProperties, opts), 25 | }); 26 | }, 27 | json: async (parsed, opts) => { 28 | return transformAndValidateUnion({ 29 | value: parsed, 30 | discriminant: parsedDiscriminant, 31 | transformedDiscriminant: rawDiscriminant, 32 | transformDiscriminantValue: (discriminantValue) => discriminantValueSchema.json(discriminantValue, { 33 | allowUnrecognizedEnumValues: opts?.allowUnrecognizedUnionMembers, 34 | }), 35 | getAdditionalPropertiesSchema: (discriminantValue) => union[discriminantValue], 36 | allowUnrecognizedUnionMembers: opts?.allowUnrecognizedUnionMembers, 37 | transformAdditionalProperties: (additionalProperties, additionalPropertiesSchema) => additionalPropertiesSchema.json(additionalProperties, opts), 38 | }); 39 | }, 40 | getType: () => SchemaType.UNION, 41 | }; 42 | return { 43 | ...baseSchema, 44 | ...getSchemaUtils(baseSchema), 45 | ...getObjectLikeUtils(baseSchema), 46 | }; 47 | } 48 | async function transformAndValidateUnion({ value, discriminant, transformedDiscriminant, transformDiscriminantValue, getAdditionalPropertiesSchema, allowUnrecognizedUnionMembers = false, transformAdditionalProperties, }) { 49 | if (!isPlainObject(value)) { 50 | return { 51 | ok: false, 52 | errors: [ 53 | { 54 | path: [], 55 | message: NOT_AN_OBJECT_ERROR_MESSAGE, 56 | }, 57 | ], 58 | }; 59 | } 60 | const { [discriminant]: discriminantValue, ...additionalProperties } = value; 61 | if (discriminantValue == null) { 62 | return { 63 | ok: false, 64 | errors: [ 65 | { 66 | path: [], 67 | message: `Missing discriminant ("${discriminant}")`, 68 | }, 69 | ], 70 | }; 71 | } 72 | const transformedDiscriminantValue = await transformDiscriminantValue(discriminantValue); 73 | if (!transformedDiscriminantValue.ok) { 74 | return { 75 | ok: false, 76 | errors: transformedDiscriminantValue.errors.map((error) => ({ 77 | path: [discriminant, ...error.path], 78 | message: error.message, 79 | })), 80 | }; 81 | } 82 | const additionalPropertiesSchema = getAdditionalPropertiesSchema(transformedDiscriminantValue.value); 83 | if (additionalPropertiesSchema == null) { 84 | if (allowUnrecognizedUnionMembers) { 85 | return { 86 | ok: true, 87 | value: { 88 | [transformedDiscriminant]: transformedDiscriminantValue.value, 89 | ...additionalProperties, 90 | }, 91 | }; 92 | } 93 | else { 94 | return { 95 | ok: false, 96 | errors: [ 97 | { 98 | path: [discriminant], 99 | message: "Unrecognized discriminant value", 100 | }, 101 | ], 102 | }; 103 | } 104 | } 105 | const transformedAdditionalProperties = await transformAdditionalProperties(additionalProperties, additionalPropertiesSchema); 106 | if (!transformedAdditionalProperties.ok) { 107 | return transformedAdditionalProperties; 108 | } 109 | return { 110 | ok: true, 111 | value: { 112 | [transformedDiscriminant]: discriminantValue, 113 | ...transformedAdditionalProperties.value, 114 | }, 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /frontend/src/api/generated/core/schemas/builders/object/object.js: -------------------------------------------------------------------------------- 1 | import { SchemaType } from "../../Schema"; 2 | import { entries } from "../../utils/entries"; 3 | import { filterObject } from "../../utils/filterObject"; 4 | import { isPlainObject, NOT_AN_OBJECT_ERROR_MESSAGE } from "../../utils/isPlainObject"; 5 | import { keys } from "../../utils/keys"; 6 | import { partition } from "../../utils/partition"; 7 | import { getObjectLikeUtils } from "../object-like"; 8 | import { getSchemaUtils } from "../schema-utils"; 9 | import { isProperty } from "./property"; 10 | export function object(schemas) { 11 | const baseSchema = { 12 | _getRawProperties: () => Promise.resolve(Object.entries(schemas).map(([parsedKey, propertySchema]) => isProperty(propertySchema) ? propertySchema.rawKey : parsedKey)), 13 | _getParsedProperties: () => Promise.resolve(keys(schemas)), 14 | parse: async (raw, opts) => { 15 | const rawKeyToProperty = {}; 16 | const requiredKeys = []; 17 | for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { 18 | const rawKey = isProperty(schemaOrObjectProperty) ? schemaOrObjectProperty.rawKey : parsedKey; 19 | const valueSchema = isProperty(schemaOrObjectProperty) 20 | ? schemaOrObjectProperty.valueSchema 21 | : schemaOrObjectProperty; 22 | const property = { 23 | rawKey, 24 | parsedKey: parsedKey, 25 | valueSchema, 26 | }; 27 | rawKeyToProperty[rawKey] = property; 28 | if (await isSchemaRequired(valueSchema)) { 29 | requiredKeys.push(rawKey); 30 | } 31 | } 32 | return validateAndTransformObject({ 33 | value: raw, 34 | requiredKeys, 35 | getProperty: (rawKey) => { 36 | const property = rawKeyToProperty[rawKey]; 37 | if (property == null) { 38 | return undefined; 39 | } 40 | return { 41 | transformedKey: property.parsedKey, 42 | transform: (propertyValue) => property.valueSchema.parse(propertyValue, opts), 43 | }; 44 | }, 45 | unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, 46 | }); 47 | }, 48 | json: async (parsed, opts) => { 49 | const requiredKeys = []; 50 | for (const [parsedKey, schemaOrObjectProperty] of entries(schemas)) { 51 | const valueSchema = isProperty(schemaOrObjectProperty) 52 | ? schemaOrObjectProperty.valueSchema 53 | : schemaOrObjectProperty; 54 | if (await isSchemaRequired(valueSchema)) { 55 | requiredKeys.push(parsedKey); 56 | } 57 | } 58 | return validateAndTransformObject({ 59 | value: parsed, 60 | requiredKeys, 61 | getProperty: (parsedKey) => { 62 | const property = schemas[parsedKey]; 63 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 64 | if (property == null) { 65 | return undefined; 66 | } 67 | if (isProperty(property)) { 68 | return { 69 | transformedKey: property.rawKey, 70 | transform: (propertyValue) => property.valueSchema.json(propertyValue, opts), 71 | }; 72 | } 73 | else { 74 | return { 75 | transformedKey: parsedKey, 76 | transform: (propertyValue) => property.json(propertyValue, opts), 77 | }; 78 | } 79 | }, 80 | unrecognizedObjectKeys: opts?.unrecognizedObjectKeys, 81 | }); 82 | }, 83 | getType: () => SchemaType.OBJECT, 84 | }; 85 | return { 86 | ...baseSchema, 87 | ...getSchemaUtils(baseSchema), 88 | ...getObjectLikeUtils(baseSchema), 89 | ...getObjectUtils(baseSchema), 90 | }; 91 | } 92 | async function validateAndTransformObject({ value, requiredKeys, getProperty, unrecognizedObjectKeys = "fail", }) { 93 | if (!isPlainObject(value)) { 94 | return { 95 | ok: false, 96 | errors: [ 97 | { 98 | path: [], 99 | message: NOT_AN_OBJECT_ERROR_MESSAGE, 100 | }, 101 | ], 102 | }; 103 | } 104 | const missingRequiredKeys = new Set(requiredKeys); 105 | const errors = []; 106 | const transformed = {}; 107 | for (const [preTransformedKey, preTransformedItemValue] of Object.entries(value)) { 108 | const property = getProperty(preTransformedKey); 109 | if (property != null) { 110 | missingRequiredKeys.delete(preTransformedKey); 111 | const value = await property.transform(preTransformedItemValue); 112 | if (value.ok) { 113 | transformed[property.transformedKey] = value.value; 114 | } 115 | else { 116 | errors.push(...value.errors.map((error) => ({ 117 | path: [preTransformedKey, ...error.path], 118 | message: error.message, 119 | }))); 120 | } 121 | } 122 | else { 123 | switch (unrecognizedObjectKeys) { 124 | case "fail": 125 | errors.push({ 126 | path: [preTransformedKey], 127 | message: `Unrecognized key "${preTransformedKey}"`, 128 | }); 129 | break; 130 | case "strip": 131 | break; 132 | case "passthrough": 133 | transformed[preTransformedKey] = preTransformedItemValue; 134 | break; 135 | } 136 | } 137 | } 138 | errors.push(...requiredKeys 139 | .filter((key) => missingRequiredKeys.has(key)) 140 | .map((key) => ({ 141 | path: [], 142 | message: `Missing required key "${key}"`, 143 | }))); 144 | if (errors.length === 0) { 145 | return { 146 | ok: true, 147 | value: transformed, 148 | }; 149 | } 150 | else { 151 | return { 152 | ok: false, 153 | errors, 154 | }; 155 | } 156 | } 157 | export function getObjectUtils(schema) { 158 | return { 159 | extend: (extension) => { 160 | const baseSchema = { 161 | _getParsedProperties: async () => [ 162 | ...(await schema._getParsedProperties()), 163 | ...(await extension._getParsedProperties()), 164 | ], 165 | _getRawProperties: async () => [ 166 | ...(await schema._getRawProperties()), 167 | ...(await extension._getRawProperties()), 168 | ], 169 | parse: async (raw, opts) => { 170 | return validateAndTransformExtendedObject({ 171 | extensionKeys: await extension._getRawProperties(), 172 | value: raw, 173 | transformBase: (rawBase) => schema.parse(rawBase, opts), 174 | transformExtension: (rawExtension) => extension.parse(rawExtension, opts), 175 | }); 176 | }, 177 | json: async (parsed, opts) => { 178 | return validateAndTransformExtendedObject({ 179 | extensionKeys: await extension._getParsedProperties(), 180 | value: parsed, 181 | transformBase: (parsedBase) => schema.json(parsedBase, opts), 182 | transformExtension: (parsedExtension) => extension.json(parsedExtension, opts), 183 | }); 184 | }, 185 | getType: () => SchemaType.OBJECT, 186 | }; 187 | return { 188 | ...baseSchema, 189 | ...getSchemaUtils(baseSchema), 190 | ...getObjectLikeUtils(baseSchema), 191 | ...getObjectUtils(baseSchema), 192 | }; 193 | }, 194 | }; 195 | } 196 | async function validateAndTransformExtendedObject({ extensionKeys, value, transformBase, transformExtension, }) { 197 | const extensionPropertiesSet = new Set(extensionKeys); 198 | const [extensionProperties, baseProperties] = partition(keys(value), (key) => extensionPropertiesSet.has(key)); 199 | const transformedBase = await transformBase(filterObject(value, baseProperties)); 200 | const transformedExtension = await transformExtension(filterObject(value, extensionProperties)); 201 | if (transformedBase.ok && transformedExtension.ok) { 202 | return { 203 | ok: true, 204 | value: { 205 | ...transformedBase.value, 206 | ...transformedExtension.value, 207 | }, 208 | }; 209 | } 210 | else { 211 | return { 212 | ok: false, 213 | errors: [ 214 | ...(transformedBase.ok ? [] : transformedBase.errors), 215 | ...(transformedExtension.ok ? [] : transformedExtension.errors), 216 | ], 217 | }; 218 | } 219 | } 220 | async function isSchemaRequired(schema) { 221 | return !(await isSchemaOptional(schema)); 222 | } 223 | async function isSchemaOptional(schema) { 224 | switch (await schema.getType()) { 225 | case SchemaType.ANY: 226 | case SchemaType.UNKNOWN: 227 | case SchemaType.OPTIONAL: 228 | return true; 229 | default: 230 | return false; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | anyio==3.6.2 ; python_version >= "3.8" and python_version < "4.0" \ 2 | --hash=sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421 \ 3 | --hash=sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3 4 | cfgv==3.3.1 ; python_version >= "3.8" and python_version < "4.0" \ 5 | --hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \ 6 | --hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736 7 | click==8.1.3 ; python_version >= "3.8" and python_version < "4.0" \ 8 | --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ 9 | --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 10 | colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0" and platform_system == "Windows" \ 11 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 12 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 13 | distlib==0.3.6 ; python_version >= "3.8" and python_version < "4.0" \ 14 | --hash=sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46 \ 15 | --hash=sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e 16 | fastapi-utils==0.2.1 ; python_version >= "3.8" and python_version < "4.0" \ 17 | --hash=sha256:0e6c7fc1870b80e681494957abf65d4f4f42f4c7f70005918e9181b22f1bd759 \ 18 | --hash=sha256:dd0be7dc7f03fa681b25487a206651d99f2330d5a567fb8ab6cb5f8a06a29360 19 | fastapi==0.79.1 ; python_version >= "3.8" and python_version < "4.0" \ 20 | --hash=sha256:006862dec0f0f5683ac21fb0864af2ff12a931e7ba18920f28cc8eceed51896b \ 21 | --hash=sha256:3c584179c64e265749e88221c860520fc512ea37e253282dab378cc503dfd7fd 22 | filelock==3.8.2 ; python_version >= "3.8" and python_version < "4.0" \ 23 | --hash=sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2 \ 24 | --hash=sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c 25 | greenlet==2.0.1 ; python_version >= "3.8" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "4.0" \ 26 | --hash=sha256:0109af1138afbfb8ae647e31a2b1ab030f58b21dd8528c27beaeb0093b7938a9 \ 27 | --hash=sha256:0459d94f73265744fee4c2d5ec44c6f34aa8a31017e6e9de770f7bcf29710be9 \ 28 | --hash=sha256:04957dc96669be041e0c260964cfef4c77287f07c40452e61abe19d647505581 \ 29 | --hash=sha256:0722c9be0797f544a3ed212569ca3fe3d9d1a1b13942d10dd6f0e8601e484d26 \ 30 | --hash=sha256:097e3dae69321e9100202fc62977f687454cd0ea147d0fd5a766e57450c569fd \ 31 | --hash=sha256:0b493db84d124805865adc587532ebad30efa68f79ad68f11b336e0a51ec86c2 \ 32 | --hash=sha256:13ba6e8e326e2116c954074c994da14954982ba2795aebb881c07ac5d093a58a \ 33 | --hash=sha256:13ebf93c343dd8bd010cd98e617cb4c1c1f352a0cf2524c82d3814154116aa82 \ 34 | --hash=sha256:1407fe45246632d0ffb7a3f4a520ba4e6051fc2cbd61ba1f806900c27f47706a \ 35 | --hash=sha256:1bf633a50cc93ed17e494015897361010fc08700d92676c87931d3ea464123ce \ 36 | --hash=sha256:2d0bac0385d2b43a7bd1d651621a4e0f1380abc63d6fb1012213a401cbd5bf8f \ 37 | --hash=sha256:3001d00eba6bbf084ae60ec7f4bb8ed375748f53aeaefaf2a37d9f0370558524 \ 38 | --hash=sha256:356e4519d4dfa766d50ecc498544b44c0249b6de66426041d7f8b751de4d6b48 \ 39 | --hash=sha256:38255a3f1e8942573b067510f9611fc9e38196077b0c8eb7a8c795e105f9ce77 \ 40 | --hash=sha256:3d75b8d013086b08e801fbbb896f7d5c9e6ccd44f13a9241d2bf7c0df9eda928 \ 41 | --hash=sha256:41b825d65f31e394b523c84db84f9383a2f7eefc13d987f308f4663794d2687e \ 42 | --hash=sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67 \ 43 | --hash=sha256:4aeaebcd91d9fee9aa768c1b39cb12214b30bf36d2b7370505a9f2165fedd8d9 \ 44 | --hash=sha256:4c8b1c43e75c42a6cafcc71defa9e01ead39ae80bd733a2608b297412beede68 \ 45 | --hash=sha256:4d37990425b4687ade27810e3b1a1c37825d242ebc275066cfee8cb6b8829ccd \ 46 | --hash=sha256:4f09b0010e55bec3239278f642a8a506b91034f03a4fb28289a7d448a67f1515 \ 47 | --hash=sha256:505138d4fa69462447a562a7c2ef723c6025ba12ac04478bc1ce2fcc279a2db5 \ 48 | --hash=sha256:5067920de254f1a2dee8d3d9d7e4e03718e8fd2d2d9db962c8c9fa781ae82a39 \ 49 | --hash=sha256:56961cfca7da2fdd178f95ca407fa330c64f33289e1804b592a77d5593d9bd94 \ 50 | --hash=sha256:5a8e05057fab2a365c81abc696cb753da7549d20266e8511eb6c9d9f72fe3e92 \ 51 | --hash=sha256:659f167f419a4609bc0516fb18ea69ed39dbb25594934bd2dd4d0401660e8a1e \ 52 | --hash=sha256:662e8f7cad915ba75d8017b3e601afc01ef20deeeabf281bd00369de196d7726 \ 53 | --hash=sha256:6f61d71bbc9b4a3de768371b210d906726535d6ca43506737682caa754b956cd \ 54 | --hash=sha256:72b00a8e7c25dcea5946692a2485b1a0c0661ed93ecfedfa9b6687bd89a24ef5 \ 55 | --hash=sha256:811e1d37d60b47cb8126e0a929b58c046251f28117cb16fcd371eed61f66b764 \ 56 | --hash=sha256:81b0ea3715bf6a848d6f7149d25bf018fd24554a4be01fcbbe3fdc78e890b955 \ 57 | --hash=sha256:88c8d517e78acdf7df8a2134a3c4b964415b575d2840a2746ddb1cc6175f8608 \ 58 | --hash=sha256:8dca09dedf1bd8684767bc736cc20c97c29bc0c04c413e3276e0962cd7aeb148 \ 59 | --hash=sha256:974a39bdb8c90a85982cdb78a103a32e0b1be986d411303064b28a80611f6e51 \ 60 | --hash=sha256:9e112e03d37987d7b90c1e98ba5e1b59e1645226d78d73282f45b326f7bddcb9 \ 61 | --hash=sha256:9e9744c657d896c7b580455e739899e492a4a452e2dd4d2b3e459f6b244a638d \ 62 | --hash=sha256:9ed358312e63bf683b9ef22c8e442ef6c5c02973f0c2a939ec1d7b50c974015c \ 63 | --hash=sha256:9f2c221eecb7ead00b8e3ddb913c67f75cba078fd1d326053225a3f59d850d72 \ 64 | --hash=sha256:a20d33124935d27b80e6fdacbd34205732660e0a1d35d8b10b3328179a2b51a1 \ 65 | --hash=sha256:a4c0757db9bd08470ff8277791795e70d0bf035a011a528ee9a5ce9454b6cba2 \ 66 | --hash=sha256:afe07421c969e259e9403c3bb658968702bc3b78ec0b6fde3ae1e73440529c23 \ 67 | --hash=sha256:b1992ba9d4780d9af9726bbcef6a1db12d9ab1ccc35e5773685a24b7fb2758eb \ 68 | --hash=sha256:b23d2a46d53210b498e5b701a1913697671988f4bf8e10f935433f6e7c332fb6 \ 69 | --hash=sha256:b5e83e4de81dcc9425598d9469a624826a0b1211380ac444c7c791d4a2137c19 \ 70 | --hash=sha256:be35822f35f99dcc48152c9839d0171a06186f2d71ef76dc57fa556cc9bf6b45 \ 71 | --hash=sha256:be9e0fb2ada7e5124f5282d6381903183ecc73ea019568d6d63d33f25b2a9000 \ 72 | --hash=sha256:c140e7eb5ce47249668056edf3b7e9900c6a2e22fb0eaf0513f18a1b2c14e1da \ 73 | --hash=sha256:c6a08799e9e88052221adca55741bf106ec7ea0710bca635c208b751f0d5b617 \ 74 | --hash=sha256:cb242fc2cda5a307a7698c93173d3627a2a90d00507bccf5bc228851e8304963 \ 75 | --hash=sha256:cce1e90dd302f45716a7715517c6aa0468af0bf38e814ad4eab58e88fc09f7f7 \ 76 | --hash=sha256:cd4ccc364cf75d1422e66e247e52a93da6a9b73cefa8cad696f3cbbb75af179d \ 77 | --hash=sha256:d21681f09e297a5adaa73060737e3aa1279a13ecdcfcc6ef66c292cb25125b2d \ 78 | --hash=sha256:d38ffd0e81ba8ef347d2be0772e899c289b59ff150ebbbbe05dc61b1246eb4e0 \ 79 | --hash=sha256:d566b82e92ff2e09dd6342df7e0eb4ff6275a3f08db284888dcd98134dbd4243 \ 80 | --hash=sha256:d5b0ff9878333823226d270417f24f4d06f235cb3e54d1103b71ea537a6a86ce \ 81 | --hash=sha256:d6ee1aa7ab36475035eb48c01efae87d37936a8173fc4d7b10bb02c2d75dd8f6 \ 82 | --hash=sha256:db38f80540083ea33bdab614a9d28bcec4b54daa5aff1668d7827a9fc769ae0a \ 83 | --hash=sha256:ea688d11707d30e212e0110a1aac7f7f3f542a259235d396f88be68b649e47d1 \ 84 | --hash=sha256:f6327b6907b4cb72f650a5b7b1be23a2aab395017aa6f1adb13069d66360eb3f \ 85 | --hash=sha256:fb412b7db83fe56847df9c47b6fe3f13911b06339c2aa02dcc09dce8bbf582cd 86 | h11==0.14.0 ; python_version >= "3.8" and python_version < "4.0" \ 87 | --hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \ 88 | --hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761 89 | identify==2.5.11 ; python_version >= "3.8" and python_version < "4.0" \ 90 | --hash=sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c \ 91 | --hash=sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db 92 | idna==3.4 ; python_version >= "3.8" and python_version < "4.0" \ 93 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 94 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 95 | nodeenv==1.7.0 ; python_version >= "3.8" and python_version < "4.0" \ 96 | --hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \ 97 | --hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b 98 | platformdirs==2.6.0 ; python_version >= "3.8" and python_version < "4.0" \ 99 | --hash=sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca \ 100 | --hash=sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e 101 | pre-commit==2.20.0 ; python_version >= "3.8" and python_version < "4.0" \ 102 | --hash=sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7 \ 103 | --hash=sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959 104 | pydantic==1.10.2 ; python_version >= "3.8" and python_version < "4.0" \ 105 | --hash=sha256:05e00dbebbe810b33c7a7362f231893183bcc4251f3f2ff991c31d5c08240c42 \ 106 | --hash=sha256:06094d18dd5e6f2bbf93efa54991c3240964bb663b87729ac340eb5014310624 \ 107 | --hash=sha256:0b959f4d8211fc964772b595ebb25f7652da3f22322c007b6fed26846a40685e \ 108 | --hash=sha256:19b3b9ccf97af2b7519c42032441a891a5e05c68368f40865a90eb88833c2559 \ 109 | --hash=sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709 \ 110 | --hash=sha256:1ee433e274268a4b0c8fde7ad9d58ecba12b069a033ecc4645bb6303c062d2e9 \ 111 | --hash=sha256:216f3bcbf19c726b1cc22b099dd409aa371f55c08800bcea4c44c8f74b73478d \ 112 | --hash=sha256:2d0567e60eb01bccda3a4df01df677adf6b437958d35c12a3ac3e0f078b0ee52 \ 113 | --hash=sha256:2e05aed07fa02231dbf03d0adb1be1d79cabb09025dd45aa094aa8b4e7b9dcda \ 114 | --hash=sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912 \ 115 | --hash=sha256:355639d9afc76bcb9b0c3000ddcd08472ae75318a6eb67a15866b87e2efa168c \ 116 | --hash=sha256:37c90345ec7dd2f1bcef82ce49b6235b40f282b94d3eec47e801baf864d15525 \ 117 | --hash=sha256:4b8795290deaae348c4eba0cebb196e1c6b98bdbe7f50b2d0d9a4a99716342fe \ 118 | --hash=sha256:5760e164b807a48a8f25f8aa1a6d857e6ce62e7ec83ea5d5c5a802eac81bad41 \ 119 | --hash=sha256:6eb843dcc411b6a2237a694f5e1d649fc66c6064d02b204a7e9d194dff81eb4b \ 120 | --hash=sha256:7b5ba54d026c2bd2cb769d3468885f23f43710f651688e91f5fb1edcf0ee9283 \ 121 | --hash=sha256:7c2abc4393dea97a4ccbb4ec7d8658d4e22c4765b7b9b9445588f16c71ad9965 \ 122 | --hash=sha256:81a7b66c3f499108b448f3f004801fcd7d7165fb4200acb03f1c2402da73ce4c \ 123 | --hash=sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410 \ 124 | --hash=sha256:9300fcbebf85f6339a02c6994b2eb3ff1b9c8c14f502058b5bf349d42447dcf5 \ 125 | --hash=sha256:9cabf4a7f05a776e7793e72793cd92cc865ea0e83a819f9ae4ecccb1b8aa6116 \ 126 | --hash=sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98 \ 127 | --hash=sha256:a4c805731c33a8db4b6ace45ce440c4ef5336e712508b4d9e1aafa617dc9907f \ 128 | --hash=sha256:ae544c47bec47a86bc7d350f965d8b15540e27e5aa4f55170ac6a75e5f73b644 \ 129 | --hash=sha256:b97890e56a694486f772d36efd2ba31612739bc6f3caeee50e9e7e3ebd2fdd13 \ 130 | --hash=sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd \ 131 | --hash=sha256:bedf309630209e78582ffacda64a21f96f3ed2e51fbf3962d4d488e503420254 \ 132 | --hash=sha256:c1ba1afb396148bbc70e9eaa8c06c1716fdddabaf86e7027c5988bae2a829ab6 \ 133 | --hash=sha256:c33602f93bfb67779f9c507e4d69451664524389546bacfe1bee13cae6dc7488 \ 134 | --hash=sha256:c4aac8e7103bf598373208f6299fa9a5cfd1fc571f2d40bf1dd1955a63d6eeb5 \ 135 | --hash=sha256:c6f981882aea41e021f72779ce2a4e87267458cc4d39ea990729e21ef18f0f8c \ 136 | --hash=sha256:cc78cc83110d2f275ec1970e7a831f4e371ee92405332ebfe9860a715f8336e1 \ 137 | --hash=sha256:d49f3db871575e0426b12e2f32fdb25e579dea16486a26e5a0474af87cb1ab0a \ 138 | --hash=sha256:dd3f9a40c16daf323cf913593083698caee97df2804aa36c4b3175d5ac1b92a2 \ 139 | --hash=sha256:e0bedafe4bc165ad0a56ac0bd7695df25c50f76961da29c050712596cf092d6d \ 140 | --hash=sha256:e9069e1b01525a96e6ff49e25876d90d5a563bc31c658289a8772ae186552236 141 | pyyaml==6.0 ; python_version >= "3.8" and python_version < "4.0" \ 142 | --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ 143 | --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ 144 | --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ 145 | --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ 146 | --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ 147 | --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ 148 | --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ 149 | --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ 150 | --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ 151 | --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ 152 | --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ 153 | --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ 154 | --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ 155 | --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ 156 | --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ 157 | --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ 158 | --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ 159 | --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ 160 | --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ 161 | --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ 162 | --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ 163 | --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ 164 | --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ 165 | --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ 166 | --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ 167 | --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ 168 | --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ 169 | --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ 170 | --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ 171 | --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ 172 | --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ 173 | --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ 174 | --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ 175 | --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ 176 | --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ 177 | --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ 178 | --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ 179 | --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ 180 | --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ 181 | --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 182 | setuptools==65.6.3 ; python_version >= "3.8" and python_version < "4.0" \ 183 | --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ 184 | --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 185 | sniffio==1.3.0 ; python_version >= "3.8" and python_version < "4.0" \ 186 | --hash=sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101 \ 187 | --hash=sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384 188 | sqlalchemy==1.4.45 ; python_version >= "3.8" and python_version < "4.0" \ 189 | --hash=sha256:01aa76f324c9bbc0dcb2bc3d9e2a9d7ede4808afa1c38d40d5e2007e3163b206 \ 190 | --hash=sha256:06055476d38ed7915eeed22b78580556d446d175c3574a01b9eb04d91f3a8b2e \ 191 | --hash=sha256:081e2a2d75466353c738ca2ee71c0cfb08229b4f9909b5fa085f75c48d021471 \ 192 | --hash=sha256:099efef0de9fbda4c2d7cb129e4e7f812007901942259d4e6c6e19bd69de1088 \ 193 | --hash=sha256:0e068b8414d60dd35d43c693555fc3d2e1d822cef07960bb8ca3f1ee6c4ff762 \ 194 | --hash=sha256:13578d1cda69bc5e76c59fec9180d6db7ceb71c1360a4d7861c37d87ea6ca0b1 \ 195 | --hash=sha256:16ad798fc121cad5ea019eb2297127b08c54e1aa95fe17b3fea9fdbc5c34fe62 \ 196 | --hash=sha256:1a92685db3b0682776a5abcb5f9e9addb3d7d9a6d841a452a17ec2d8d457bea7 \ 197 | --hash=sha256:26b8424b32eeefa4faad21decd7bdd4aade58640b39407bf43e7d0a7c1bc0453 \ 198 | --hash=sha256:29a29d02c9e6f6b105580c5ed7afb722b97bc2e2fdb85e1d45d7ddd8440cfbca \ 199 | --hash=sha256:2d1539fbc82d2206380a86d6d7d0453764fdca5d042d78161bbfb8dd047c80ec \ 200 | --hash=sha256:2d6f178ff2923730da271c8aa317f70cf0df11a4d1812f1d7a704b1cf29c5fe3 \ 201 | --hash=sha256:2db887dbf05bcc3151de1c4b506b14764c6240a42e844b4269132a7584de1e5f \ 202 | --hash=sha256:416fe7d228937bd37990b5a429fd00ad0e49eabcea3455af7beed7955f192edd \ 203 | --hash=sha256:445914dcadc0b623bd9851260ee54915ecf4e3041a62d57709b18a0eed19f33b \ 204 | --hash=sha256:52b90c9487e4449ad954624d01dea34c90cd8c104bce46b322c83654f37a23c5 \ 205 | --hash=sha256:55ddb5585129c5d964a537c9e32a8a68a8c6293b747f3fa164e1c034e1657a98 \ 206 | --hash=sha256:561605cfc26273825ed2fb8484428faf36e853c13e4c90c61c58988aeccb34ed \ 207 | --hash=sha256:5953e225be47d80410ae519f865b5c341f541d8e383fb6d11f67fb71a45bf890 \ 208 | --hash=sha256:6a91b7883cb7855a27bc0637166eed622fdf1bb94a4d1630165e5dd88c7e64d3 \ 209 | --hash=sha256:6cd53b4c756a6f9c6518a3dc9c05a38840f9ae442c91fe1abde50d73651b6922 \ 210 | --hash=sha256:715f5859daa3bee6ecbad64501637fa4640ca6734e8cda6135e3898d5f8ccadd \ 211 | --hash=sha256:7e32ce2584564d9e068bb7e0ccd1810cbb0a824c0687f8016fe67e97c345a637 \ 212 | --hash=sha256:88f4ad3b081c0dbb738886f8d425a5d983328670ee83b38192687d78fc82bd1e \ 213 | --hash=sha256:96821d806c0c90c68ce3f2ce6dd529c10e5d7587961f31dd5c30e3bfddc4545d \ 214 | --hash=sha256:9a21c1fb71c69c8ec65430160cd3eee44bbcea15b5a4e556f29d03f246f425ec \ 215 | --hash=sha256:9b7025d46aba946272f6b6b357a22f3787473ef27451f342df1a2a6de23743e3 \ 216 | --hash=sha256:a3bcd5e2049ceb97e8c273e6a84ff4abcfa1dc47b6d8bbd36e07cce7176610d3 \ 217 | --hash=sha256:a62ae2ea3b940ce9c9cbd675489c2047921ce0a79f971d3082978be91bd58117 \ 218 | --hash=sha256:a87f8595390764db333a1705591d0934973d132af607f4fa8b792b366eacbb3c \ 219 | --hash=sha256:c8051bff4ce48cbc98f11e95ac46bfd1e36272401070c010248a3230d099663f \ 220 | --hash=sha256:ca152ffc7f0aa069c95fba46165030267ec5e4bb0107aba45e5e9e86fe4d9363 \ 221 | --hash=sha256:cd95a3e6ab46da2c5b0703e797a772f3fab44d085b3919a4f27339aa3b1f51d3 \ 222 | --hash=sha256:d458fd0566bc9e10b8be857f089e96b5ca1b1ef033226f24512f9ffdf485a8c0 \ 223 | --hash=sha256:db3ccbce4a861bf4338b254f95916fc68dd8b7aa50eea838ecdaf3a52810e9c0 \ 224 | --hash=sha256:dc10423b59d6d032d6dff0bb42aa06dc6a8824eb6029d70c7d1b6981a2e7f4d8 \ 225 | --hash=sha256:e91a5e45a2ea083fe344b3503405978dff14d60ef3aa836432c9ca8cd47806b6 \ 226 | --hash=sha256:f1d3fb02a4d0b07d1351a4a52f159e5e7b3045c903468b7e9349ebf0020ffdb9 \ 227 | --hash=sha256:f61e54b8c2b389de1a8ad52394729c478c67712dbdcdadb52c2575e41dae94a5 \ 228 | --hash=sha256:f7944b04e6fcf8d733964dd9ee36b6a587251a1a4049af3a9b846f6e64eb349a \ 229 | --hash=sha256:fd69850860093a3f69fefe0ab56d041edfdfe18510b53d9a2eaecba2f15fa795 230 | starlette==0.19.1 ; python_version >= "3.8" and python_version < "4.0" \ 231 | --hash=sha256:5a60c5c2d051f3a8eb546136aa0c9399773a689595e099e0877704d5888279bf \ 232 | --hash=sha256:c6d21096774ecb9639acad41b86b7706e52ba3bf1dc13ea4ed9ad593d47e24c7 233 | toml==0.10.2 ; python_version >= "3.8" and python_version < "4.0" \ 234 | --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ 235 | --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f 236 | typing-extensions==4.4.0 ; python_version >= "3.8" and python_version < "4.0" \ 237 | --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \ 238 | --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e 239 | uvicorn==0.18.3 ; python_version >= "3.8" and python_version < "4.0" \ 240 | --hash=sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af \ 241 | --hash=sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b 242 | virtualenv==20.17.1 ; python_version >= "3.8" and python_version < "4.0" \ 243 | --hash=sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4 \ 244 | --hash=sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058 245 | --------------------------------------------------------------------------------