diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e384528 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +DisableFormat: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5a4f47..c6c3b6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,18 +1,16 @@ -name: CI +name: Build on: push: tags: - v* - pull_request: - branches: - - master concurrency: group: build-${{ github.head_ref }} cancel-in-progress: true env: + SKBUILD_CMAKE_BUILD_TYPE: "Release" CIBW_SKIP: > pp* diff --git a/.github/workflows/memory_check.yml b/.github/workflows/memory_check.yml index 24d5ce7..5dd646f 100644 --- a/.github/workflows/memory_check.yml +++ b/.github/workflows/memory_check.yml @@ -1,4 +1,4 @@ -name: Memory Check +name: Memory Problems on: push: @@ -12,10 +12,13 @@ env: PYTHONUNBUFFERED: "1" FORCE_COLOR: "1" PYTHONIOENCODING: "utf8" + PYTHONMALLOC: "malloc" + PYTHONDEVMODE: "1" + HATCH_VERBOSE: "1" jobs: run: - name: Valgrind on Ubuntu + name: Run memory tests on Ubuntu runs-on: ubuntu-latest steps: @@ -26,16 +29,19 @@ jobs: with: python-version: 3.12 - - name: Install PyTest + - name: Install Pytest run: | - pip install pytest pytest-asyncio + pip install pytest pytest-asyncio pytest-memray shell: bash - - name: Build project + - name: Build view.py run: pip install .[full] - name: Install Valgrind run: sudo apt-get update && sudo apt-get -y install valgrind - name: Run tests with Valgrind - run: valgrind --suppressions=valgrind-python.supp --error-exitcode=1 pytest -x + run: valgrind --error-exitcode=1 pytest + + - name: Run tests with Memray + run: pytest --enable-leak-tracking diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 548eefb..57b079e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,8 @@ env: PYTHONUNBUFFERED: "1" FORCE_COLOR: "1" PYTHONIOENCODING: "utf8" + PYTHONDEVMODE: "1" + HATCH_VERBOSE: "1" jobs: run: @@ -24,8 +26,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-12] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v2 @@ -35,23 +37,11 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install PyTest - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pip install pytest pytest-asyncio - else - pip install pytest pytest-asyncio pytest-memray - fi - shell: bash - - - name: Build project + - name: Install Pytest + run: pip install pytest pytest-asyncio + + - name: Build view.py run: pip install .[full] - - name: Run tests - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pytest - else - pytest --memray - fi - shell: bash + - name: Run tests + run: pytest -x diff --git a/.gitignore b/.gitignore index 9820253..6432420 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,47 @@ +# Python __pycache__/ .venv/ -ext/ -test.py -sample.py +38venv/ +39venv/ +311venv/ + +# LSP +.vscode/ compile_flags.txt +pyawaitable.h + +# View Configurations view.toml view.json view_config.py -/app.py + +# Testing Files +*.test +test.py a.py +.coverage +.pytest-cache/ +.ruff-cache/ +.cache/ + +# Logs *.log +vgcore.* +valgrind.txt* + +# JavaScript node_modules/ *.lock -site/ benchmark.py +package-lock.json +client.js +.next/ + +# Builds +site/ dist/ -*.egg-info build/ -*.so +*.egg-info/ html/dist.css -38venv -39venv -311venv +*.so a.md -new_app/ -vgcore.* -.vscode/ -*.test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dac792..4a9d5a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for coroutines in `PyAwaitable` (vendored) - Finished websocket implementation - Added the `custom` loader -- Added support for returning `bytes` objects in the body. +- Added support for returning `bytes` objects in the body +- Added `nosanitize` and `repr` to the `ref` attribute of `` tags in view template rendering +- `WebSocketDisconnect` is now raised instead of `WebSocketHandshakeError` in an unexpected WebSocket disconnect +- Added many, _many_, more docstrings +- Added the `app` attribute to `Context` +- Switched to PyMalloc under the hood +- Deprecated the `run()` utility +- Added support for asynchronous `__view_result__` functions +- Removed unstable `components` functions from top-level `view` module +- Added native support for `ReactPy` component routes +- Added the `expect_errors` utility +- Added the `HeaderDict` class +- Changed the `headers` attribute on `Context` to a `HeaderDict` instance of a `dict` +- Added the `call_result` utility +- Added the `ctx` parameter to `to_response` +- Removed broken hint when forgetting to call `load()` +- Added support for `isinstance` to `SupportsViewResult` +- Moved `to_response` to the `view.utils` module +- Added the `force` parameter to `run` +- Added the `view dev` command (live reload) +- Fixed redirection and disabling of HTTP server logging +- C API is now compliant with [PEP 7](https://peps.python.org/pep-0007/) +- Added `-g3` and `-O3` flag to the `_view` extension module (debugging information and optimizations) +- Removed use of Rich `escape()` in the message shown when a dependency is needed +- Query string client errors are now displayed during development mode +- `KeyboardInterrupt` is swallowed by the server coroutine, and a log message is now issued +- Typecode API now raises exceptions indicating a validation error (and now it's sent as a response with a query or body parse failure) +- `hatchling` and `scikit-build-core` are now used for build instead of `setuptools` +- Renamed the `view-admin` command to `view-py` +- **Breaking Change:** Renamed `Error` to `HTTPError` +- **Breaking Change:** `__view_result__` is now given a `Context` parameter +- **Breaking Change:** `to_response` is now asynchronous +- **Breaking Change:** Renamed `Response._custom` to `Response.translate_body` - **Breaking Change:** Removed the `hijack` configuration setting -- **Breaking Change:** Removed the `post_init` parameter from `new_app`, as well as renamed the `store_address` parameter to `store`. +- **Breaking Change:** Removed the `post_init` parameter from `new_app`, as well as renamed the `store_address` parameter to `store` - **Breaking Change:** `load()` now takes routes via variadic arguments, instead of a list of routes. ## [1.0.0-alpha10] - 2024-5-26 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..226c987 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.15...3.26) +project(${SKBUILD_PROJECT_NAME} LANGUAGES C) + +message(STATUS "CMAKE_BUILD_TYPE set to '${CMAKE_BUILD_TYPE}'") + +# Source code +file(GLOB _view_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/src/_view/*.c +) +MESSAGE(DEBUG ${_view_SRC}) + +# Find Python +find_package( + Python + COMPONENTS Interpreter Development.Module + REQUIRED) + +# Link Python +python_add_library(_view MODULE ${_view_SRC} WITH_SOABI) + +# Settings +add_compile_definitions(PYAWAITABLE_PYAPI) + +# Add include directories +target_include_directories(_view PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include/) +target_include_directories(_view PUBLIC $ENV{PYAWAITABLE_INCLUDE_DIR}) + +MESSAGE(STATUS "Everything looks good, let's install!") +# Install extension module +install(TARGETS _view DESTINATION .) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb0fda9..9ffacee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,13 +20,14 @@ $ pip install . Congratulations, you have just started your development with view.py! +Note that this cannot be an editable install (the `-e` flag), as `scikit-build-core` does not support it. + ## Workflow First, you should create a new branch: ``` -$ git branch my_cool_feature -$ git checkout my_cool_feature +$ git switch -c my-cool-feature ``` All of your code should be contained on this branch. @@ -72,7 +73,7 @@ fancy = false server_logger = true ``` -These settings will stop view.py's fancy output from showing, as well as stopping the hijack of the ASGI logger, and you'll get the raw output. +These settings will stop view.py's fancy output from showing, as well as stopping the hijack of the server's logger, and you'll get the raw server output. ## Writing Tests diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d01997f..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -graft include -prune tests -prune build diff --git a/_view.pyi b/_view.pyi index 25ad827..c18d25c 100644 --- a/_view.pyi +++ b/_view.pyi @@ -1,7 +1,12 @@ # flake8: noqa -# NOTE: anything in this file that is defined solely for typing purposes should be -# prefixed with __ to tell the developer that its not an actual symbol defined by -# the extension module + +""" +_view - Type stubs for the view.py extension module. + +Anything in this file that is defined solely for typing purposes should be +prefixed with __ to tell the developer that its not an actual symbol defined by +the extension module. +""" from ipaddress import IPv4Address as __IPv4Address from ipaddress import IPv6Address as __IPv6Address @@ -14,6 +19,7 @@ from typing import Literal as __Literal from typing import NoReturn as __NoReturn from typing import TypeVar as __TypeVar +from view.app import App from view.routing import RouteData as __RouteData from view.typing import AsgiDict as __AsgiDict from view.typing import AsgiReceive as __AsgiReceive @@ -119,13 +125,16 @@ class ViewApp: def _supply_parsers(self, query: __Parser, json: __Parser, /) -> None: ... def _register_error(self, error: type, /) -> None: ... -def test_awaitable(coro: __Coroutine[__Any, __Any, __T], /) -> __Awaitable[__T]: ... +def test_awaitable( + coro: __Coroutine[__Any, __Any, __T], / +) -> __Awaitable[__T]: ... class Context: def __init__(self) -> __NoReturn: ... + app: App cookies: dict[str, str] - headers: dict[str, str] + headers: HeaderDict client: __IPv4Address | __IPv6Address | None server: __IPv4Address | __IPv6Address | None method: __StrMethodASGI @@ -152,5 +161,14 @@ class ViewWebSocket: class InvalidStatusError(RuntimeError): ... class WebSocketHandshakeError(RuntimeError): ... -def setup_route_log(func: __Callable[[int | str, str, str], None]) -> None: ... -def register_ws_cls(tp: type[__Any]) -> None: ... +def setup_route_log(func: __Callable[[int | str, str, str], None], warn: __Callable[[str], None], /) -> None: ... +def register_ws_cls( + tp: type[__Any], ws_handshake_err: type[__Any], ws_err: type[__Any], /, +) -> None: ... + +class HeaderDict: + def __init__(self) -> __NoReturn: ... + def __setitem__(self, key: str, value: str, /) -> None: ... + def __getitem__(self, key: str, /) -> str | list[str]: ... + +def dummy_context(app: ViewApp | None) -> Context: ... diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..a18c018 --- /dev/null +++ b/client/index.html @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..87a6a47 --- /dev/null +++ b/client/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "typescript": "^5.4.5", + "vite": "^5.2.0", + "vite-plugin-singlefile": "^2.0.1" + }, + "dependencies": { + "@reactpy/client": "^0.3.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/client/src/reactpy.tsx b/client/src/reactpy.tsx new file mode 100644 index 0000000..fabba91 --- /dev/null +++ b/client/src/reactpy.tsx @@ -0,0 +1,247 @@ +import { + BaseReactPyClient, + ReactPyClient, + ReactPyModule, +} from "@reactpy/client"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { Layout } from "@reactpy/client/src/components"; + +export function createReconnectingWebSocket(props: { + url: URL; + readyPromise: Promise; + onOpen?: () => void; + onMessage: (message: MessageEvent) => void; + onClose?: () => void; + startInterval: number; + maxInterval: number; + maxRetries: number; + backoffMultiplier: number; +}) { + const { startInterval, maxInterval, maxRetries, backoffMultiplier } = props; + let retries = 0; + let interval = startInterval; + let everConnected = false; + const closed = false; + const socket: { current?: WebSocket } = {}; + + const connect = () => { + if (closed) { + return; + } + socket.current = new WebSocket(props.url); + socket.current.onopen = () => { + everConnected = true; + console.info("ReactPy connected!"); + interval = startInterval; + retries = 0; + if (props.onOpen) { + props.onOpen(); + } + }; + socket.current.onmessage = props.onMessage; + socket.current.onclose = () => { + if (props.onClose) { + props.onClose(); + } + if (!everConnected) { + console.info("ReactPy failed to connect!"); + return; + } + console.info("ReactPy disconnected!"); + if (retries >= maxRetries) { + console.info("ReactPy connection max retries exhausted!"); + return; + } + console.info( + `ReactPy reconnecting in ${(interval / 1000).toPrecision( + 4, + )} seconds...`, + ); + setTimeout(connect, interval); + interval = nextInterval(interval, backoffMultiplier, maxInterval); + retries++; + }; + }; + + props.readyPromise + .then(() => console.info("Starting ReactPy client...")) + .then(connect); + + return socket; +} + +export function nextInterval( + currentInterval: number, + backoffMultiplier: number, + maxInterval: number, +): number { + return Math.min( + // increase interval by backoff multiplier + currentInterval * backoffMultiplier, + // don't exceed max interval + maxInterval, + ); +} + +export type ReconnectOptions = { + startInterval: number; + maxInterval: number; + maxRetries: number; + backoffMultiplier: number; +}; + +export type ReactPyUrls = { + componentUrl: URL; + query: string; + jsModules: string; +}; + +export type ReactPyDjangoClientProps = { + urls: ReactPyUrls; + reconnectOptions: ReconnectOptions; + mountElement: HTMLElement; + prerenderElement: HTMLElement | null; + offlineElement: HTMLElement | null; +}; + +export class ReactPyDjangoClient + extends BaseReactPyClient + implements ReactPyClient +{ + urls: ReactPyUrls; + socket: { current?: WebSocket }; + mountElement: HTMLElement; + prerenderElement: HTMLElement | null = null; + offlineElement: HTMLElement | null = null; + + constructor(props: ReactPyDjangoClientProps) { + super(); + this.urls = props.urls; + this.socket = createReconnectingWebSocket({ + readyPromise: this.ready, + url: this.urls.componentUrl, + onMessage: async ({ data }) => this.handleIncoming(JSON.parse(data)), + ...props.reconnectOptions, + onClose: () => { + // If offlineElement exists, show it and hide the mountElement/prerenderElement + if (this.prerenderElement) { + this.prerenderElement.remove(); + this.prerenderElement = null; + } + if (this.offlineElement) { + this.mountElement.hidden = true; + this.offlineElement.hidden = false; + } + }, + onOpen: () => { + // If offlineElement exists, hide it and show the mountElement + if (this.offlineElement) { + this.offlineElement.hidden = true; + this.mountElement.hidden = false; + } + }, + }); + this.mountElement = props.mountElement; + this.prerenderElement = props.prerenderElement; + this.offlineElement = props.offlineElement; + } + + sendMessage(message: any): void { + this.socket.current?.send(JSON.stringify(message)); + } + + loadModule(moduleName: string): Promise { + return import(`${this.urls.jsModules}/${moduleName}`); + } +} + +export function mountComponent( + mountElement: HTMLElement, + host: string, + urlPrefix: string, + routeId: string, + resolvedJsModulesPath: string, + reconnectStartInterval: number, + reconnectMaxInterval: number, + reconnectMaxRetries: number, + reconnectBackoffMultiplier: number, +) { + // Protocols + let httpProtocol = window.location.protocol; + let wsProtocol = `ws${httpProtocol === "https:" ? "s" : ""}:`; + + // WebSocket route (for Python components) + let wsOrigin: string; + if (host) { + wsOrigin = `${wsProtocol}//${host}`; + } else { + wsOrigin = `${wsProtocol}//${window.location.host}`; + } + + // HTTP route (for JavaScript modules) + let httpOrigin: string; + let jsModulesPath: string; + if (host) { + httpOrigin = `${httpProtocol}//${host}`; + jsModulesPath = `${urlPrefix}/web_module`; + } else { + httpOrigin = `${httpProtocol}//${window.location.host}`; + if (resolvedJsModulesPath) { + jsModulesPath = resolvedJsModulesPath; + } else { + jsModulesPath = `${urlPrefix}/web_module`; + } + } + + // Embed the initial HTTP path into the WebSocket URL + let componentUrl = new URL(`${wsOrigin}/${urlPrefix}`); + componentUrl.searchParams.append("route", routeId); + if (window.location.search) { + componentUrl.searchParams.append("http_search", window.location.search); + } + + // Configure a new ReactPy client + const client = new ReactPyDjangoClient({ + urls: { + componentUrl: componentUrl, + query: document.location.search, + jsModules: `${httpOrigin}/${jsModulesPath}`, + }, + reconnectOptions: { + startInterval: reconnectStartInterval, + maxInterval: reconnectMaxInterval, + backoffMultiplier: reconnectBackoffMultiplier, + maxRetries: reconnectMaxRetries, + }, + mountElement: mountElement, + prerenderElement: document.getElementById(mountElement.id + "-prerender"), + offlineElement: document.getElementById(mountElement.id + "-offline"), + }); + + // Replace the prerender element with the real element on the first layout update + if (client.prerenderElement) { + client.onMessage("layout-update", () => { + if (client.prerenderElement) { + client.prerenderElement.replaceWith(client.mountElement); + client.prerenderElement = null; + } + }); + } + + // Start rendering the component + const root = ReactDOM.createRoot(client.mountElement); + root.render(); +} + +mountComponent( + document.documentElement, + window.location.host, + "_view/reactpy-stream", + document.getElementById("_view-route-hook")!.innerText, + "", + 750, + 60000, + 150, + 1.25, +); diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/client/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 0000000..3f0914b --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "jsx": "react" + }, + "include": ["src"] +} diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..4b4db39 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +export default defineConfig({ + plugins: [viteSingleFile()], +}); diff --git a/docs/building-projects/responses.md b/docs/building-projects/responses.md index 7c67939..eb28206 100644 --- a/docs/building-projects/responses.md +++ b/docs/building-projects/responses.md @@ -114,7 +114,7 @@ app = new_app() class MyObject: def __view_result__(self): - return "Hello from MyObject!", {"x-www-myobject": "foo"} + return "Hello from MyObject!", 201, {"x-www-myobject": "foo"} @app.get("/") async def index(): @@ -127,7 +127,7 @@ Note that in the above scenario, you wouldn't actually need a whole object. Inst ```py def _response(): - return "Hello, view.py!", {"foo": "bar"} + return "Hello, view.py!", 201, {"foo": "bar"} @app.get("/") async def index(): @@ -227,7 +227,7 @@ class MyResponse(Response[str]): Generally, you'll want to use the `custom` translation strategy when writing custom `Response` objects. -You must implement the `_custom` method (which takes in the `T` passed to `Response`, and returns a `str`) to use the `custom` strategy. For example, the code below would be for a `Response` type that formats a list: +You must implement the `translate_body` method (which takes in the `T` passed to `Response`, and returns a `str`) to use the `custom` strategy. For example, the code below would be for a `Response` type that formats a list: ```py from view import Response @@ -236,7 +236,7 @@ class ListResponse(Response[list]): def __init__(self, body: list) -> None: super().__init__(body, body_translate="custom") - def _custom(self, body: list) -> str: + def translate_body(self, body: list) -> str: return " ".join(body) ``` diff --git a/docs/getting-started/creating_a_project.md b/docs/getting-started/creating_a_project.md index 86e7f8b..6fe4374 100644 --- a/docs/getting-started/creating_a_project.md +++ b/docs/getting-started/creating_a_project.md @@ -18,7 +18,7 @@ $ pipx run view-py init view.py doesn't actually need any big project structure. In fact, you can run an app in just a single Python file, but larger structures like this might be more convenient for big projects. The only real requirement for something to be a view app is that it calls `new_app`, but again, more on that later. -Some "hello world" code for manually starting a view project would look like this: +Some "hello world" code for manually starting a view.py project would look like this: ```py from view import new_app diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 17f57c6..3dc8ef5 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -40,7 +40,7 @@ $ view !!! note Problem on Linux - On Linux, `view` is already a command! Read about it [here](https://www.ibm.com/docs/zh/aix/7.2?topic=v-view-command), but in short, it opens `vi` in read only mode. You can either shadow this command with view.py's CLI, or use the `view-admin` command instead, which is an alias. This documentation will assume you use `view` instead of `view-admin`, but note that they do the exact same thing. + On Linux, `view` is already a command! Read about it [here](https://www.ibm.com/docs/zh/aix/7.2?topic=v-view-command), but in short, it opens `vi` in read only mode. You can either shadow this command with view.py's CLI, or use the `view-py` command instead, which is an alias. This documentation will assume you use `view` instead of `view-py`, but note that they do the exact same thing. If this doesn't work properly, try executing via Python: diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts new file mode 100644 index 0000000..4f11a03 --- /dev/null +++ b/docs/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/hatch.toml b/hatch.toml new file mode 100644 index 0000000..ddc50be --- /dev/null +++ b/hatch.toml @@ -0,0 +1,51 @@ +[version] +path = "src/view/__about__.py" + +[build.targets.sdist] +only-include = ["src/", "_view.pyi"] + +[build.targets.wheel] +packages = ["src/view"] + +[build.targets.wheel.force-include] +"_view.pyi" = "_view.pyi" + +[metadata.hooks.custom] +path = "hatch_build.py" +enable-by-default = true + +[build.targets.wheel.hooks.scikit-build] +experimental = true + +[build.targets.wheel.hooks.scikit-build.cmake] +source-dir = "." +build-type = "Debug" +verbose = true + +[build.targets.wheel.hooks.scikit-build.install] +strip = false + +[envs.hatch-test] +features = ["full"] +dev-mode = false +dependencies = [ + "coverage", + "pytest", + "pytest-memray", + "pytest-asyncio", +] +platforms = ["linux", "macos"] + +[envs.test.overrides.platform.windows] +dependencies = [ + "coverage", + "pytest", + "pytest-asyncio", +] + +[envs.docs] +dependencies = ["mkdocs", "mkdocstrings[python]", "mkdocs-material", "mkdocs-git-revision-date-localized-plugin"] + +[envs.docs.scripts] +build = "mkdocs build" +serve = "mkdocs serve" diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000..360d1f1 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,7 @@ +from hatchling.metadata.plugin.interface import MetadataHookInterface +import pyawaitable +import os + +class JSONMetaDataHook(MetadataHookInterface): + def update(self, *_) -> None: + os.environ["PYAWAITABLE_INCLUDE_DIR"] = pyawaitable.include() diff --git a/html/base.html b/html/base.html deleted file mode 100644 index cd818d3..0000000 --- a/html/base.html +++ /dev/null @@ -1,391 +0,0 @@ - - - - - - - {% if page.title %}{{ page.title }} - {% endif %}{{ config.site_name - }} - - - - - - - - - - - {% for js in extra_javascript %} - - {% endfor %} - - - - -
-
- - -
-
-
-
-
- -
-
- - {% block content %} {% endblock %} - -
-
- - - - - -
- - ->>>>>>> 2732cf7344a5c028353852ec0c019f1525467a76 -
-
-
-
- - {% block content %} {% endblock %} - -
-
- - - - - -
- - -
- - - diff --git a/html/home.html b/html/home.html deleted file mode 100644 index 71939a5..0000000 --- a/html/home.html +++ /dev/null @@ -1,191 +0,0 @@ -{% extends "base.html" %} {% block content %} - -
-
-
-

-
- -
- -
- -
-
- $ - pip install -U - view.py - -
-
-
-

- Use view.py as a micro web framework -

-
from view import new_app
-
-app = new_app()
-
-@app.get("/")
-@app.query("greeting", str, default="Hello")
-def index(greeting: str):
-    return f"{greeting}, view.py!"
-
-app.run()
-
-
-

...Or as fullstack

-
# routes/index.py
-from view import get, query, template
-
-@get()
-@query("greeting", str, default="Hello")
-async def index(greeting: str):
-    # greeting is automatically accessible via the template
-    return await template("index")
-
-# Filesystem Based Routing
-
-
-
-
-
-
-
-
-

- Affiliated With -

- Space Hosting -
-
-
-
-
-
-
- -{% endblock %} diff --git a/html/icons/discord-mark-black.svg b/html/icons/discord-mark-black.svg deleted file mode 100644 index f9fd918..0000000 --- a/html/icons/discord-mark-black.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/html/icons/discord-mark-white.svg b/html/icons/discord-mark-white.svg deleted file mode 100644 index 7f9a31f..0000000 --- a/html/icons/discord-mark-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/html/icons/github-mark-white.svg b/html/icons/github-mark-white.svg deleted file mode 100644 index d5e6491..0000000 --- a/html/icons/github-mark-white.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/html/icons/github-mark.svg b/html/icons/github-mark.svg deleted file mode 100644 index f82a0ca..0000000 --- a/html/icons/github-mark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/html/icons/space_hosting.svg b/html/icons/space_hosting.svg deleted file mode 100644 index 5327bba..0000000 --- a/html/icons/space_hosting.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/html/icons/space_hosting_cropped.png b/html/icons/space_hosting_cropped.png deleted file mode 100644 index 9b2794a..0000000 Binary files a/html/icons/space_hosting_cropped.png and /dev/null differ diff --git a/html/icons/view_logo.png b/html/icons/view_logo.png deleted file mode 100644 index c5a2d48..0000000 Binary files a/html/icons/view_logo.png and /dev/null differ diff --git a/html/logo.png b/html/logo.png deleted file mode 100644 index af90c53..0000000 Binary files a/html/logo.png and /dev/null differ diff --git a/html/main.css b/html/main.css deleted file mode 100644 index bf2d0f5..0000000 --- a/html/main.css +++ /dev/null @@ -1,328 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -:root { - color-scheme: dark; -} - -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@700&display=swap"); - -@media (prefers-color-scheme: dark) { -} - -@font-face { - font-family: "Modern Deco"; - src: url("/modern_deco.ttf"); -} - -@font-face { - font-family: "Geist"; - src: url("/geist.ttf"); -} - -body { - font-family: "Geist", sans-serif; -} - -.modern-deco { - font-family: "Modern Deco", "Geist", sans-serif; -} - -.hljs { - @apply border-zinc-100 border rounded-lg; -} - -a { - @apply text-rose-600 hover:text-rose-500 transition-all dark:text-rose-600 dark:hover:text-rose-500; -} - -.custom-shape-divider-bottom-1706484168 { - position: relative; - bottom: 0; - left: 0; - width: 100%; - overflow: hidden; - line-height: 0; - transform: rotate(180deg); -} - -.custom-shape-divider-bottom-1706484168 svg { - position: relative; - display: block; - width: calc(100% + 1.3px); - height: 297px; - transform: rotateY(180deg); -} - -.custom-shape-divider-bottom-1706484168 .shape-fill { - fill: #18181b; -} - -hr { - @apply border-t border-zinc-100 dark:border-zinc-900; -} - -h1 { - font-family: "Geist", "Inter", sans-serif; - @apply py-6 dark:text-5xl text-5xl dark:font-extrabold font-extrabold dark:bg-gradient-to-t bg-gradient-to-t from-zinc-400 to-black inline-block dark:inline-block dark:text-transparent dark:bg-clip-text text-transparent bg-clip-text dark:from-zinc-500 dark:to-zinc-50; -} - -.headerlink { - @apply pl-2 text-zinc-400 hover:text-rose-500 dark:text-zinc-400 dark:hover:text-rose-500; -} - -h2 { - @apply pt-2 pb-2 dark:text-4xl text-4xl dark:font-extrabold font-extrabold dark:bg-gradient-to-t bg-gradient-to-t from-zinc-400 to-black inline-block dark:inline-block dark:text-transparent dark:bg-clip-text text-transparent bg-clip-text dark:from-zinc-500 dark:to-zinc-50; -} - -h3 { - @apply pt-2 pb-2 dark:text-2xl text-2xl dark:font-extrabold font-extrabold dark:bg-gradient-to-t bg-gradient-to-t from-zinc-400 to-black inline-block dark:inline-block dark:text-transparent dark:bg-clip-text text-transparent bg-clip-text dark:from-zinc-500 dark:to-zinc-50; -} - -.gradient { - @apply bg-gradient-to-r from-rose-400 to-rose-700 inline-block text-transparent bg-clip-text font-bold dark:from-rose-600 dark:to-rose-800; -} - -pre { - @apply py-1 dark:py-1; -} - -p { - @apply py-1 leading-normal dark:py-1 dark:leading-normal dark:text-zinc-200 text-lg; -} - -code { - @apply dark:text-white bg-zinc-100 dark:bg-zinc-950 dark:border-zinc-900 rounded-lg p-1 border border-zinc-200 text-sm; -} - -ul { - @apply list-disc dark:list-disc dark:pl-6 flex flex-col space-y-2 py-2; -} - -.headerlink { - @apply text-zinc-600; -} - -.headerlink.doc { - @apply text-xs text-center; -} - -.on-this-page { - @apply text-zinc-500 dark:text-zinc-400; -} - -.doc-object { - @apply border-zinc-100 border p-4 rounded-lg dark:border-zinc-900; -} - -.doc-children { - @apply py-2 flex flex-col space-y-3; -} - -table { - @apply w-full text-sm text-left rtl:text-right text-zinc-500 overflow-x-auto dark:text-zinc-400; -} - -thead { - @apply text-base text-zinc-700 uppercase bg-zinc-50 select-none cursor-pointer dark:text-zinc-300; -} - -th { - @apply px-6 py-3; -} - -tr { - @apply odd:bg-white even:bg-zinc-50 border-b dark:odd:bg-black dark:even:bg-zinc-950 dark:border-black; -} - -td { - @apply px-6 py-4; -} - -.example { - @apply pt-4; -} - -.admonition { -<<<<<<< HEAD - @apply border-zinc-100 border-[3px] rounded-lg dark:border-zinc-900; -======= - @apply border-zinc-100 border rounded-lg dark:border-zinc-900 py-2 bg-black w-1/3; ->>>>>>> 2732cf7344a5c028353852ec0c019f1525467a76 -} - -.admonition-title { - @apply !text-lg !text-white; -} - -.admonition p { - @apply px-4 text-zinc-400 text-sm; -} - -.admonition pre { - @apply text-xs p-2; -} - -<<<<<<< HEAD -.admonition.question { - @apply border-emerald-300 dark:border-emerald-700; -} - -.question .admonition-title { - @apply text-white; -} - -.admonition.tip { - @apply border-green-300 dark:border-green-600; -} - - -.admonition.info { - @apply border-blue-300 dark:border-blue-600; -} - - -.admonition.note { - @apply border-indigo-300 dark:border-indigo-600; -} - -.admonition.danger { - @apply border-rose-300 dark:border-rose-600; -} - - -.admonition.warning { - @apply border-orange-300 dark:border-orange-600; -} - - -======= ->>>>>>> 2732cf7344a5c028353852ec0c019f1525467a76 -.doc-label-async code { - @apply bg-indigo-100 text-indigo-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-indigo-500; -} - -.doc-label-module-attribute code { - @apply bg-sky-100 text-sky-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-sky-500; -} - -.doc-label-instance-attribute code { - @apply bg-lime-100 text-lime-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-lime-500; -} - -.doc-label-class-attribute code { - @apply bg-pink-100 text-pink-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-pink-500; -} - -.doc-label-classmethod code { - @apply bg-violet-100 text-violet-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-violet-500; -} - -.doc-contents p { - @apply text-zinc-500 dark:text-zinc-300; -} - -.doc-contents strong { - @apply uppercase font-normal text-zinc-400 dark:text-zinc-300; -} - -.doc-heading { - @apply text-base dark:text-base; -} - -.n { - @apply text-black dark:text-white; -} - -.o { - @apply text-black dark:text-zinc-400; -} - -.p { - @apply text-black dark:text-zinc-400; -} - -.fn { - @apply text-teal-500 dark:text-teal-400; -} - -.nb { - @apply text-teal-500 dark:text-teal-400; -} - -.kc { - @apply text-indigo-500 dark:text-indigo-400; -} - -.mi { - @apply text-green-500 dark:text-green-400; -} - -.s1 { - @apply text-green-500 dark:text-green-400; -} - -.Typewriter__cursor { - @apply text-white; -} - -#mkdocs-search-results article { - @apply rounded-lg border border-zinc-100 dark:border-zinc-900 p-2; -} - -#mkdocs-search-results a { - @apply font-bold text-xl; -} - -#mkdocs-search-results p { - @apply text-ellipsis italic break-words py-0 dark:text-white; -} - -#mkdocs-search-results p::after { - content: "..."; -} - -.doc-contents { - @apply overflow-x-auto; -} - -.doc-symbol-function { - @apply bg-red-100 text-red-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-red-500; -} - -.doc-symbol-function::before { - content: "func"; -} - -.doc-symbol-class { - @apply bg-amber-100 text-amber-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-amber-500; -} - -.doc-symbol-class::before { - content: "class"; -} - -.doc-symbol-attribute { - @apply bg-cyan-100 text-cyan-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-cyan-500; -} - -.doc-symbol-attribute::before { - content: "attr"; -} - -.doc-symbol-method { - @apply bg-blue-100 text-blue-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-blue-500; -} - -.doc-symbol-method::before { - content: "method"; -} - -.doc-symbol-module { - @apply bg-teal-100 text-teal-500 rounded-lg p-1 font-bold border-0 dark:bg-black dark:border dark:border-zinc-900 dark:text-teal-500; -} - -.doc-symbol-module::before { - content: "mod"; -} diff --git a/html/main.html b/html/main.html deleted file mode 100644 index c69055d..0000000 --- a/html/main.html +++ /dev/null @@ -1,226 +0,0 @@ -{% extends "base.html" %} {% block content %} -<<<<<<< HEAD -
-