Add support for custom components starter (#2314)

This commit is contained in:
Martin Xu 2024-02-28 15:25:26 -08:00 committed by GitHub
parent c465b94016
commit 4f9cdd6472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 828 additions and 119 deletions

View File

@ -5,7 +5,7 @@ branch = true
[report]
show_missing = true
# TODO bump back to 79
fail_under = 69
fail_under = 68
precision = 2
# Regexes for lines to exclude from consideration

181
poetry.lock generated
View File

@ -1,10 +1,9 @@
# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "alembic"
version = "1.13.1"
description = "A database migration tool for SQLAlchemy."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -26,7 +25,6 @@ tz = ["backports.zoneinfo"]
name = "anyio"
version = "4.3.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -49,7 +47,6 @@ trio = ["trio (>=0.23)"]
name = "async-timeout"
version = "4.0.3"
description = "Timeout context manager for asyncio programs"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -61,7 +58,6 @@ files = [
name = "asynctest"
version = "0.13.0"
description = "Enhance the standard unittest package with features for testing asyncio libraries"
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -73,7 +69,6 @@ files = [
name = "attrs"
version = "23.2.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -93,7 +88,6 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p
name = "bidict"
version = "0.23.1"
description = "The bidirectional mapping library for Python."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -105,7 +99,6 @@ files = [
name = "black"
version = "22.12.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -137,11 +130,34 @@ d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "build"
version = "1.0.3"
description = "A simple, correct Python build frontend"
optional = false
python-versions = ">= 3.7"
files = [
{file = "build-1.0.3-py3-none-any.whl", hash = "sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"},
{file = "build-1.0.3.tar.gz", hash = "sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b"},
]
[package.dependencies]
colorama = {version = "*", markers = "os_name == \"nt\""}
importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
packaging = ">=19.0"
pyproject_hooks = "*"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
[package.extras]
docs = ["furo (>=2023.08.17)", "sphinx (>=7.0,<8.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)", "sphinx-issues (>=3.0.0)"]
test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "setuptools (>=56.0.0)", "setuptools (>=67.8.0)", "wheel (>=0.36.0)"]
typing = ["importlib-metadata (>=5.1)", "mypy (>=1.5.0,<1.6.0)", "tomli", "typing-extensions (>=3.7.4.3)"]
virtualenv = ["virtualenv (>=20.0.35)"]
[[package]]
name = "certifi"
version = "2024.2.2"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -153,7 +169,6 @@ files = [
name = "cffi"
version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -218,7 +233,6 @@ pycparser = "*"
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -230,7 +244,6 @@ files = [
name = "charset-normalizer"
version = "3.3.2"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@ -330,7 +343,6 @@ files = [
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -345,7 +357,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "cloudpickle"
version = "2.2.1"
description = "Extended pickling support for Python objects"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -357,7 +368,6 @@ files = [
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -369,7 +379,6 @@ files = [
name = "coverage"
version = "7.4.1"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -437,7 +446,6 @@ toml = ["tomli"]
name = "darglint"
version = "1.8.1"
description = "A utility for ensuring Google-style docstrings stay up to date with the source code."
category = "dev"
optional = false
python-versions = ">=3.6,<4.0"
files = [
@ -449,7 +457,6 @@ files = [
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -461,7 +468,6 @@ files = [
name = "distro"
version = "1.9.0"
description = "Distro - an OS platform information API"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -473,7 +479,6 @@ files = [
name = "docopt"
version = "0.6.2"
description = "Pythonic argument parser, that will make you smile"
category = "main"
optional = false
python-versions = "*"
files = [
@ -484,7 +489,6 @@ files = [
name = "exceptiongroup"
version = "1.2.0"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -499,7 +503,6 @@ test = ["pytest (>=6)"]
name = "fastapi"
version = "0.96.1"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -521,7 +524,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6
name = "filelock"
version = "3.13.1"
description = "A platform independent file lock."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -538,7 +540,6 @@ typing = ["typing-extensions (>=4.8)"]
name = "greenlet"
version = "3.0.3"
description = "Lightweight in-process concurrent programming"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -610,7 +611,6 @@ test = ["objgraph", "psutil"]
name = "gunicorn"
version = "20.1.0"
description = "WSGI HTTP Server for UNIX"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -631,7 +631,6 @@ tornado = ["tornado (>=0.2)"]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -643,7 +642,6 @@ files = [
name = "httpcore"
version = "1.0.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -658,14 +656,13 @@ h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<0.24.0)"]
[[package]]
name = "httpx"
version = "0.25.2"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -676,21 +673,20 @@ files = [
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = ">=1.0.0,<2.0.0"
httpcore = "==1.*"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
socks = ["socksio (==1.*)"]
[[package]]
name = "identify"
version = "2.5.35"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -705,7 +701,6 @@ license = ["ukkonen"]
name = "idna"
version = "3.6"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
@ -717,7 +712,6 @@ files = [
name = "importlib-metadata"
version = "7.0.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -737,7 +731,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs
name = "importlib-resources"
version = "6.1.1"
description = "Read resources from Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -756,7 +749,6 @@ testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)",
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -768,7 +760,6 @@ files = [
name = "jinja2"
version = "3.1.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -786,7 +777,6 @@ i18n = ["Babel (>=2.7)"]
name = "mako"
version = "1.3.2"
description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -806,7 +796,6 @@ testing = ["pytest"]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -831,7 +820,6 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
name = "markupsafe"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -901,7 +889,6 @@ files = [
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -913,7 +900,6 @@ files = [
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
@ -925,7 +911,6 @@ files = [
name = "nodeenv"
version = "1.8.0"
description = "Node.js virtual environment builder"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
files = [
@ -940,7 +925,6 @@ setuptools = "*"
name = "numpy"
version = "1.24.4"
description = "Fundamental package for array computing in Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -978,7 +962,6 @@ files = [
name = "numpy"
version = "1.26.4"
description = "Fundamental package for array computing in Python"
category = "dev"
optional = false
python-versions = ">=3.9"
files = [
@ -1024,7 +1007,6 @@ files = [
name = "outcome"
version = "1.3.0.post0"
description = "Capture the outcome of Python function calls."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1039,7 +1021,6 @@ attrs = ">=19.2.0"
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1051,7 +1032,6 @@ files = [
name = "pandas"
version = "1.5.3"
description = "Powerful data structures for data analysis, time series, and statistics"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1096,7 +1076,6 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"]
name = "pandas"
version = "2.2.0"
description = "Powerful data structures for data analysis, time series, and statistics"
category = "dev"
optional = false
python-versions = ">=3.9"
files = [
@ -1169,7 +1148,6 @@ xml = ["lxml (>=4.9.2)"]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1181,7 +1159,6 @@ files = [
name = "pillow"
version = "10.2.0"
description = "Python Imaging Library (Fork)"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1267,7 +1244,6 @@ xmp = ["defusedxml"]
name = "pipdeptree"
version = "2.14.0"
description = "Command line utility to show dependency tree of packages."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -1283,7 +1259,6 @@ test = ["covdefaults (>=2.3)", "diff-cover (>=8.0.1)", "pip (>=23.3.1)", "pytest
name = "pipreqs"
version = "0.4.13"
description = "Pip requirements.txt generator based on imports in project"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1299,7 +1274,6 @@ yarg = "*"
name = "platformdirs"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1315,7 +1289,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co
name = "plotly"
version = "5.19.0"
description = "An open-source, interactive data visualization library for Python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1331,7 +1304,6 @@ tenacity = ">=6.2.0"
name = "pluggy"
version = "1.4.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1347,7 +1319,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1366,7 +1337,6 @@ virtualenv = ">=20.10.0"
name = "psutil"
version = "5.9.8"
description = "Cross-platform lib for process and system monitoring in Python."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
files = [
@ -1395,7 +1365,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
name = "py-cpuinfo"
version = "9.0.0"
description = "Get CPU info with pure Python"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1407,7 +1376,6 @@ files = [
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -1419,7 +1387,6 @@ files = [
name = "pydantic"
version = "1.10.14"
description = "Data validation and settings management using python type hints"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1472,7 +1439,6 @@ email = ["email-validator (>=1.0.3)"]
name = "pygments"
version = "2.17.2"
description = "Pygments is a syntax highlighting package written in Python."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1484,11 +1450,24 @@ files = [
plugins = ["importlib-metadata"]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyproject-hooks"
version = "1.0.0"
description = "Wrappers to call pyproject.toml-based build backend hooks."
optional = false
python-versions = ">=3.7"
files = [
{file = "pyproject_hooks-1.0.0-py3-none-any.whl", hash = "sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8"},
{file = "pyproject_hooks-1.0.0.tar.gz", hash = "sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"},
]
[package.dependencies]
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
[[package]]
name = "pyright"
version = "1.1.334"
description = "Command line wrapper for pyright"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1507,7 +1486,6 @@ dev = ["twine (>=3.4.1)"]
name = "pysocks"
version = "1.7.1"
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -1520,7 +1498,6 @@ files = [
name = "pytest"
version = "7.4.4"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1543,7 +1520,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
name = "pytest-asyncio"
version = "0.20.3"
description = "Pytest support for asyncio"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1562,7 +1538,6 @@ testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy
name = "pytest-benchmark"
version = "4.0.0"
description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1583,7 +1558,6 @@ histogram = ["pygal", "pygaljs"]
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1602,7 +1576,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale
name = "pytest-mock"
version = "3.12.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1620,7 +1593,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
files = [
@ -1635,7 +1607,6 @@ six = ">=1.5"
name = "python-engineio"
version = "4.9.0"
description = "Engine.IO server and client for Python"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -1655,7 +1626,6 @@ docs = ["sphinx"]
name = "python-multipart"
version = "0.0.5"
description = "A streaming multipart parser for Python"
category = "main"
optional = false
python-versions = "*"
files = [
@ -1669,7 +1639,6 @@ six = ">=1.4.0"
name = "python-socketio"
version = "5.11.1"
description = "Socket.IO server and client for Python"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -1690,7 +1659,6 @@ docs = ["sphinx"]
name = "pytz"
version = "2024.1"
description = "World timezone definitions, modern and historical"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1702,7 +1670,6 @@ files = [
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
files = [
@ -1752,7 +1719,6 @@ files = [
name = "redis"
version = "4.6.0"
description = "Python client for Redis database and key-value store"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1771,7 +1737,6 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"
name = "reflex-hosting-cli"
version = "0.1.8"
description = "Reflex Hosting CLI"
category = "main"
optional = false
python-versions = ">=3.8,<4.0"
files = [
@ -1796,7 +1761,6 @@ websockets = ">=10.4"
name = "requests"
version = "2.31.0"
description = "Python HTTP for Humans."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1818,7 +1782,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
name = "rich"
version = "13.7.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@ -1838,7 +1801,6 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
name = "ruff"
version = "0.0.244"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -1864,7 +1826,6 @@ files = [
name = "selenium"
version = "4.18.0"
description = ""
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -1881,26 +1842,24 @@ urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
[[package]]
name = "setuptools"
version = "69.1.0"
version = "69.1.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"},
{file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"},
{file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
{file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "simple-websocket"
version = "1.0.0"
description = "Simple WebSocket server and client for Python"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -1918,7 +1877,6 @@ docs = ["sphinx"]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@ -1930,7 +1888,6 @@ files = [
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -1942,7 +1899,6 @@ files = [
name = "sortedcontainers"
version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -1954,7 +1910,6 @@ files = [
name = "sqlalchemy"
version = "2.0.27"
description = "Database Abstraction Library"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2042,7 +1997,6 @@ sqlcipher = ["sqlcipher3_binary"]
name = "sqlmodel"
version = "0.0.14"
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
@ -2058,7 +2012,6 @@ SQLAlchemy = ">=2.0.0,<2.1.0"
name = "starlette"
version = "0.27.0"
description = "The little ASGI library that shines."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2077,7 +2030,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam
name = "starlette-admin"
version = "0.9.0"
description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2100,7 +2052,6 @@ test = ["aiomysql (>=0.1.1,<0.2.0)", "aiosqlite (>=0.17.0,<0.20.0)", "arrow (>=1
name = "tabulate"
version = "0.9.0"
description = "Pretty-print tabular data"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2115,7 +2066,6 @@ widechars = ["wcwidth"]
name = "tenacity"
version = "8.2.3"
description = "Retry code until it succeeds"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2130,7 +2080,6 @@ doc = ["reno", "sphinx", "tornado (>=4.5)"]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
@ -2142,7 +2091,6 @@ files = [
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2154,7 +2102,6 @@ files = [
name = "trio"
version = "0.24.0"
description = "A friendly Python library for async concurrency and I/O"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2175,7 +2122,6 @@ sortedcontainers = "*"
name = "trio-websocket"
version = "0.11.1"
description = "WebSocket library for Trio"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2192,7 +2138,6 @@ wsproto = ">=0.14"
name = "typer"
version = "0.9.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -2214,7 +2159,6 @@ test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.
name = "types-tabulate"
version = "0.9.0.20240106"
description = "Typing stubs for tabulate"
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
@ -2226,7 +2170,6 @@ files = [
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2238,7 +2181,6 @@ files = [
name = "tzdata"
version = "2024.1"
description = "Provider of IANA time zone data"
category = "dev"
optional = false
python-versions = ">=2"
files = [
@ -2250,7 +2192,6 @@ files = [
name = "urllib3"
version = "2.2.1"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2271,7 +2212,6 @@ zstd = ["zstandard (>=0.18.0)"]
name = "uvicorn"
version = "0.20.0"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2290,7 +2230,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
name = "uvicorn"
version = "0.24.0.post1"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2309,7 +2248,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
name = "virtualenv"
version = "20.25.0"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -2330,7 +2268,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess
name = "watchdog"
version = "2.3.1"
description = "Filesystem events monitoring"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -2371,7 +2308,6 @@ watchmedo = ["PyYAML (>=3.10)"]
name = "watchfiles"
version = "0.19.0"
description = "Simple, modern and high performance file watching and code reload in python."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -2406,7 +2342,6 @@ anyio = ">=3.0.0"
name = "websockets"
version = "12.0"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2484,11 +2419,24 @@ files = [
{file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"},
]
[[package]]
name = "wheel"
version = "0.42.0"
description = "A built-package format for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "wheel-0.42.0-py3-none-any.whl", hash = "sha256:177f9c9b0d45c47873b619f5b650346d632cdc35fb5e4d25058e09c9e581433d"},
{file = "wheel-0.42.0.tar.gz", hash = "sha256:c45be39f7882c9d34243236f2d63cbd58039e360f85d0913425fbd7ceea617a8"},
]
[package.extras]
test = ["pytest (>=6.0.0)", "setuptools (>=65)"]
[[package]]
name = "wrapt"
version = "1.16.0"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -2568,7 +2516,6 @@ files = [
name = "wsproto"
version = "1.2.0"
description = "WebSockets state-machine based protocol implementation"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
@ -2583,7 +2530,6 @@ h11 = ">=0.9.0,<1"
name = "yarg"
version = "0.1.9"
description = "A semi hard Cornish cheese, also queries PyPI (PyPI client)"
category = "main"
optional = false
python-versions = "*"
files = [
@ -2598,7 +2544,6 @@ requests = "*"
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
@ -2613,4 +2558,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "c22317cf6beac82268e73619e984788895d258bb7b7983eacdfbf6093c419dc3"
content-hash = "840901e82824445e708eb44ae7f237b286946db2dd3332740c6d1e5e891fb84d"

View File

@ -59,6 +59,9 @@ packaging = "^23.1"
pipdeptree = "^2.13.0"
reflex-hosting-cli = ">=0.1.2"
charset-normalizer = "^3.3.2"
wheel = "^0.42.0"
build = "^1.0.3"
setuptools = "^69.1.1"
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.2"

View File

@ -0,0 +1,9 @@
# {{ module_name }}
A Reflex custom component {{ module_name }}.
## Installation
```bash
pip install {{ package_name }}
```

View File

@ -0,0 +1 @@
from .{{ module_name }} import *

View File

@ -0,0 +1,36 @@
"""Welcome to Reflex! This file showcases the custom component in a basic app."""
from rxconfig import config
import reflex as rx
from {{ custom_component_module_dir }} import {{ module_name }}
filename = f"{config.app_name}/{config.app_name}.py"
class State(rx.State):
"""The app state."""
pass
def index() -> rx.Component:
return rx.center(
rx.theme_panel(),
rx.vstack(
rx.heading("Welcome to Reflex!", size="9"),
rx.text("Test your custom component by editing ", rx.code(filename)),
{{ module_name }}(),
align="center",
spacing="7",
font_size="2em",
),
height="100vh",
)
# Add state and page to the app.
app = rx.App()
app.add_page(index)

View File

@ -0,0 +1,35 @@
[build-system]
requires = [
"setuptools",
"wheel",
]
build-backend = "setuptools.build_meta"
[project]
name = "{{ package_name }}"
version = "0.0.1"
description = "Reflex custom component {{ module_name }}"
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.8"
authors = [{ name = "Your Name", email = "YOUREMAIL@domain.com" }]
keywords = [
"reflex",
"reflex-custom-components"]
dependencies = [
"reflex>=0.4.2"
]
classifiers = [
"Development Status :: 4 - Beta",
]
[project.urls]
Homepage = "https://github.com"
[project.optional-dependencies]
dev = ["build", "twine"]
[tool.setuptools.packages.find]
where = ["custom_components"]

View File

@ -0,0 +1,57 @@
"""Reflex custom component {{ component_class_name }}."""
# For wrapping react guide, visit https://reflex.dev/docs/wrapping-react/overview/
import reflex as rx
# Some libraries you may want to wrap may require dynamic imports.
# This is because they they may not be compatible with Server-Side Rendering (SSR).
# To handle this in Reflex all you need to do is subclass NoSSRComponent instead.
# For example:
# from reflex.components.component import NoSSRComponent
# class {{ component_class_name }}(NoSSRComponent):
# pass
class {{ component_class_name }}(rx.Component):
"""{{ component_class_name }} component."""
# The React library to wrap.
library = "Fill-Me"
# The React component tag.
tag = "Fill-Me"
# If the tag is the default export from the module, you must set is_default = True.
# This is normally used when components don't have curly braces around them when importing.
# is_default = True
# If you are wrapping another components with the same tag as a component in your project
# you can use aliases to differentiate between them and avoid naming conflicts.
# alias = "Other{{ component_class_name }}"
# The props of the React component.
# Note: when Reflex compiles the component to Javascript,
# `snake_case` property names are automatically formatted as `camelCase`.
# The prop names may be defined in `camelCase` as well.
# some_prop: rx.Var[str] = "some default value"
# some_other_prop: rx.Var[int] = 1
# By default Reflex will install the library you have specified in the library property.
# However, sometimes you may need to install other libraries to use a component.
# In this case you can use the lib_dependencies property to specify other libraries to install.
# lib_dependencies: list[str] = []
# Event triggers, I did not understand the wording of the doc.
# def get_event_triggers(self) -> dict[str, Any]:
# return {
# **super().get_event_triggers(),
# "on_change": lambda e0: [e0],
# }
# To add custom code to your component
# def _get_custom_code(self) -> str:
# return "const customCode = 'customCode';"
{{ module_name }} = {{ component_class_name }}.create

View File

@ -98,3 +98,20 @@ STYLE = get_template("web/styles/styles.css.jinja2")
# Code that generate the package json file
PACKAGE_JSON = get_template("web/package.json.jinja2")
# Code that generate the pyproject.toml file for custom components
CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
"custom_components/pyproject.toml.jinja2"
)
# Code that generates the README file for custom components
CUSTOM_COMPONENTS_README = get_template("custom_components/README.md.jinja2")
# Code that generates the source file for custom components
CUSTOM_COMPONENTS_SOURCE = get_template("custom_components/src.py.jinja2")
# Code that generates the init file for custom components
CUSTOM_COMPONENTS_INIT_FILE = get_template("custom_components/__init__.py.jinja2")
# Code that generates the demo app main py file for testing custom components
CUSTOM_COMPONENTS_DEMO_APP = get_template("custom_components/demo_app.py.jinja2")

View File

@ -40,6 +40,9 @@ from .config import (
GitIgnore,
RequirementsTxt,
)
from .custom_components import (
CustomComponents,
)
from .event import Endpoint, EventTriggers, SocketEvent
from .installer import (
Bun,
@ -67,6 +70,7 @@ __ALL__ = [
Config,
COOKIES,
ComponentName,
CustomComponents,
DefaultPage,
Dirs,
Endpoint,

View File

@ -0,0 +1,30 @@
"""Constants for the custom components."""
from __future__ import annotations
from types import SimpleNamespace
class CustomComponents(SimpleNamespace):
"""Constants for the custom components."""
# The name of the custom components source directory.
SRC_DIR = "custom_components"
# The name of the custom components pyproject.toml file.
PYPROJECT_TOML = "pyproject.toml"
# The name of the custom components package README file.
PACKAGE_README = "README.md"
# The name of the custom components package .gitignore file.
PACKAGE_GITIGNORE = ".gitignore"
# The name of the distribution directory as result of a build.
DIST_DIR = "dist"
# The name of the init file.
INIT_FILE = "__init__.py"
# Suffixes for the distribution files.
DISTRIBUTION_FILE_SUFFIXES = [".tar.gz", ".whl"]
# The name to the URL of python package repositories.
REPO_URLS = {
# Note: the trailing slash is required for below URLs.
"pypi": "https://upload.pypi.org/legacy/",
"testpypi": "https://test.pypi.org/legacy/",
}

View File

@ -0,0 +1 @@
"""The Reflex custom components."""

View File

@ -0,0 +1,565 @@
"""CLI for creating custom components."""
from __future__ import annotations
import os
import re
import subprocess
import sys
from collections import namedtuple
from contextlib import contextmanager
from pathlib import Path
from typing import Optional
import typer
from reflex import constants
from reflex.config import get_config
from reflex.constants import CustomComponents
from reflex.utils import console
config = get_config()
custom_components_cli = typer.Typer()
@contextmanager
def set_directory(working_directory: str):
"""Context manager that sets the working directory.
Args:
working_directory: The working directory to change to.
Yields:
Yield to the caller to perform operations in the working directory.
"""
current_directory = os.getcwd()
try:
os.chdir(working_directory)
yield
finally:
os.chdir(current_directory)
def _create_package_config(module_name: str, package_name: str):
"""Create a package config pyproject.toml file.
Args:
module_name: The name of the module.
package_name: The name of the package typically constructed with `reflex-` prefix and a meaningful library name.
"""
from reflex.compiler import templates
with open(CustomComponents.PYPROJECT_TOML, "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_PYPROJECT_TOML.render(
module_name=module_name, package_name=package_name
)
)
def _create_readme(module_name: str, package_name: str):
"""Create a package README file.
Args:
module_name: The name of the module.
package_name: The name of the python package to be published.
"""
from reflex.compiler import templates
with open(CustomComponents.PACKAGE_README, "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_README.render(
module_name=module_name,
package_name=package_name,
)
)
def _write_source_and_init_py(
custom_component_src_dir: str,
component_class_name: str,
module_name: str,
):
"""Write the source code and init file from templates for the custom component.
Args:
custom_component_src_dir: The name of the custom component source directory.
component_class_name: The name of the component class.
module_name: The name of the module.
"""
from reflex.compiler import templates
with open(
os.path.join(
custom_component_src_dir,
f"{module_name}.py",
),
"w",
) as f:
f.write(
templates.CUSTOM_COMPONENTS_SOURCE.render(
component_class_name=component_class_name, module_name=module_name
)
)
with open(
os.path.join(
custom_component_src_dir,
CustomComponents.INIT_FILE,
),
"w",
) as f:
f.write(templates.CUSTOM_COMPONENTS_INIT_FILE.render(module_name=module_name))
def _populate_demo_app(name_variants: NameVariants):
"""Populate the demo app that imports the custom components.
Args:
name_variants: the tuple including various names such as package name, class name needed for the project.
"""
from reflex import constants
from reflex.compiler import templates
from reflex.reflex import _init
demo_app_dir = name_variants.demo_app_dir
demo_app_name = name_variants.demo_app_name
console.info(f"Creating app for testing: {demo_app_dir}")
os.makedirs(demo_app_dir)
with set_directory(demo_app_dir):
# We start with the blank template as basis.
_init(name=demo_app_name, template=constants.Templates.Kind.BLANK)
# Then overwrite the app source file with the one we want for testing custom components.
# This source file is rendered using jinja template file.
with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f:
f.write(
templates.CUSTOM_COMPONENTS_DEMO_APP.render(
custom_component_module_dir=name_variants.custom_component_module_dir,
module_name=name_variants.module_name,
)
)
def _get_default_library_name_parts() -> list[str]:
"""Get the default library name. Based on the current directory name, remove any non-alphanumeric characters.
Raises:
ValueError: If the current directory name is not suitable for python projects, and we cannot find a valid library name based off it.
Returns:
The parts of default library name.
"""
current_dir_name = os.getcwd().split(os.path.sep)[-1]
cleaned_dir_name = re.sub("[^0-9a-zA-Z-_]+", "", current_dir_name)
parts = re.split("-|_", cleaned_dir_name)
if not parts:
# The folder likely has a name not suitable for python paths.
raise ValueError(
f"Could not find a valid library name based on the current directory: got {current_dir_name}."
)
return parts
NameVariants = namedtuple(
"NameVariants",
[
"library_name",
"component_class_name",
"package_name",
"module_name",
"custom_component_module_dir",
"demo_app_dir",
"demo_app_name",
],
)
def _validate_library_name(library_name: str | None) -> NameVariants:
"""Validate the library name.
Args:
library_name: The name of the library if picked otherwise None.
Raises:
Exit: If the library name is not suitable for python projects.
Returns:
A tuple containing the various names such as package name, class name, etc., needed for the project.
"""
if library_name is not None and not re.match(
r"^[a-zA-Z-]+[a-zA-Z0-9-]*$", library_name
):
console.error(
f"Please use only alphanumeric characters or dashes: got {library_name}"
)
raise typer.Exit(code=1)
# If not specified, use the current directory name to form the module name.
name_parts = (
[part.lower() for part in library_name.split("-")]
if library_name
else _get_default_library_name_parts()
)
if not library_name:
library_name = "-".join(name_parts)
# Component class name is the camel case.
component_class_name = "".join([part.capitalize() for part in name_parts])
console.info(f"Component class name: {component_class_name}")
# Package name is commonly kebab case.
package_name = f"reflex-{library_name}"
console.info(f"Package name: {package_name}")
# Module name is the snake case.
module_name = "_".join(name_parts)
custom_component_module_dir = f"reflex_{module_name}"
console.info(f"Custom component source directory: {custom_component_module_dir}")
# Use the same name for the directory and the app.
demo_app_dir = demo_app_name = f"{module_name}_demo"
console.info(f"Demo app directory: {demo_app_dir}")
return NameVariants(
library_name=library_name,
component_class_name=component_class_name,
package_name=package_name,
module_name=module_name,
custom_component_module_dir=custom_component_module_dir,
demo_app_dir=demo_app_dir,
demo_app_name=demo_app_name,
)
def _populate_custom_component_project(name_variants: NameVariants):
"""Populate the custom component source directory. This includes the pyproject.toml, README.md, and the code template for the custom component.
Args:
name_variants: the tuple including various names such as package name, class name needed for the project.
"""
console.info(
f"Populating pyproject.toml with package name: {name_variants.package_name}"
)
# write pyproject.toml, README.md, etc.
_create_package_config(
module_name=name_variants.library_name, package_name=name_variants.package_name
)
_create_readme(
module_name=name_variants.library_name, package_name=name_variants.package_name
)
console.info(
f"Initializing the component directory: {CustomComponents.SRC_DIR}/{name_variants.custom_component_module_dir}"
)
os.makedirs(CustomComponents.SRC_DIR)
with set_directory(CustomComponents.SRC_DIR):
os.makedirs(name_variants.custom_component_module_dir)
_write_source_and_init_py(
custom_component_src_dir=name_variants.custom_component_module_dir,
component_class_name=name_variants.component_class_name,
module_name=name_variants.module_name,
)
@custom_components_cli.command(name="init")
def init(
library_name: Optional[str] = typer.Option(
None,
help="The name of your library. On PyPI, package will be published as `reflex-{library-name}`.",
),
install: bool = typer.Option(
True,
help="Whether to install package from this local custom component in editable mode.",
),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Initialize a custom component.
Args:
library_name: The name of the library.
install: Whether to install package from this local custom component in editable mode.
loglevel: The log level to use.
Raises:
Exit: If the pyproject.toml already exists.
"""
from reflex.utils import exec, prerequisites
console.set_log_level(loglevel)
if os.path.exists(CustomComponents.PYPROJECT_TOML):
console.error(f"A {CustomComponents.PYPROJECT_TOML} already exists. Aborting.")
typer.Exit(code=1)
# Show system info.
exec.output_system_info()
# Check the name follows the convention if picked.
name_variants = _validate_library_name(library_name)
_populate_custom_component_project(name_variants)
_populate_demo_app(name_variants)
# Initialize the .gitignore.
prerequisites.initialize_gitignore()
if install:
package_name = name_variants.package_name
console.info(f"Installing {package_name} in editable mode.")
if _pip_install_on_demand(package_name=".", install_args=["-e"]):
console.info(f"Package {package_name} installed!")
else:
raise typer.Exit(code=1)
console.print("Custom component initialized successfully!")
console.print("Here's the summary:")
console.print(
f"{CustomComponents.PYPROJECT_TOML} and {CustomComponents.PACKAGE_README} created. [bold]Please fill in details such as your name, email, homepage URL.[/bold]"
)
console.print(
f"Source code template is in {CustomComponents.SRC_DIR}. [bold]Start by editing it with your component implementation.[/bold]"
)
console.print(
f"Demo app created in {name_variants.demo_app_dir}. [bold]Use this app to test your custom component.[/bold]"
)
def _pip_install_on_demand(
package_name: str,
install_args: list[str] | None = None,
) -> bool:
"""Install a package on demand.
Args:
package_name: The name of the package.
install_args: The additional arguments for the pip install command.
Returns:
True if the package is installed successfully, False otherwise.
"""
install_args = install_args or []
install_cmds = [
sys.executable,
"-m",
"pip",
"install",
*install_args,
package_name,
]
console.debug(f"Install package: {' '.join(install_cmds)}")
return _run_commands_in_subprocess(install_cmds)
def _run_commands_in_subprocess(cmds: list[str]) -> bool:
"""Run commands in a subprocess.
Args:
cmds: The commands to run.
Returns:
True if the command runs successfully, False otherwise.
"""
console.debug(f"Running command: {' '.join(cmds)}")
try:
result = subprocess.run(cmds, capture_output=True, text=True, check=True)
console.debug(result.stdout)
return True
except subprocess.CalledProcessError as cpe:
console.error(cpe.stdout)
console.error(cpe.stderr)
return False
@custom_components_cli.command(name="build")
def build(
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Build a custom component. Must be run from the project root directory where the pyproject.toml is.
Args:
loglevel: The log level to use.
Raises:
Exit: If the build fails.
"""
console.set_log_level(loglevel)
console.print("Building custom component...")
cmds = [sys.executable, "-m", "build", "."]
if _run_commands_in_subprocess(cmds):
console.info("Custom component built successfully!")
else:
raise typer.Exit(code=1)
def _validate_repository_name(repository: str | None) -> str:
"""Validate the repository name.
Args:
repository: The name of the repository.
Returns:
The name of the repository.
Raises:
Exit: If the repository name is not supported.
"""
if repository is None:
return "pypi"
elif repository not in CustomComponents.REPO_URLS:
console.error(
f"Unsupported repository name. Allow {CustomComponents.REPO_URLS.keys()}, got {repository}"
)
raise typer.Exit(code=1)
return repository
def _validate_credentials(
username: str | None, password: str | None, token: str | None
) -> tuple[str, str]:
"""Validate the credentials.
Args:
username: The username to use for authentication on python package repository.
password: The password to use for authentication on python package repository.
token: The token to use for authentication on python package repository.
Raises:
Exit: If the appropriate combination of credentials is not provided.
Returns:
The username and password.
"""
if token is not None:
if username is not None or password is not None:
console.error("Cannot use token and username/password at the same time.")
raise typer.Exit(code=1)
username = "__token__"
password = token
elif username is None or password is None:
console.error(
"Must provide both username and password for authentication if not using a token."
)
raise typer.Exit(code=1)
return username, password
def _ensure_dist_dir():
"""Ensure the distribution directory and the expected files exist.
Raises:
Exit: If the distribution directory does not exist or the expected files are not found.
"""
dist_dir = Path(CustomComponents.DIST_DIR)
# Check if the distribution directory exists.
if not dist_dir.exists():
console.error(f"Directory {dist_dir.name} does not exist. Please build first.")
raise typer.Exit(code=1)
# Check if the distribution directory is indeed a directory.
if not dist_dir.is_dir():
console.error(
f"{dist_dir.name} is not a directory. If this is a file you added, move it and rebuild."
)
raise typer.Exit(code=1)
# Check if the distribution files exist.
for suffix in CustomComponents.DISTRIBUTION_FILE_SUFFIXES:
if not list(dist_dir.glob(f"*{suffix}")):
console.error(
f"Expected distribution file with suffix {suffix} in directory {dist_dir.name}"
)
raise typer.Exit(code=1)
@custom_components_cli.command(name="publish")
def publish(
repository: Optional[str] = typer.Option(
None,
"-r",
"--repository",
help="The name of the repository. Defaults to pypi. Only supports pypi and testpypi (Test PyPI) for now.",
),
token: Optional[str] = typer.Option(
None,
"-t",
"--token",
help="The API token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time",
),
username: Optional[str] = typer.Option(
None,
"-u",
"--username",
help="The username to use for authentication on python package repository. Username and password must both be provided.",
),
password: Optional[str] = typer.Option(
None,
"-p",
"--password",
help="The password to use for authentication on python package repository. Username and password must both be provided.",
),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Publish a custom component. Must be run from the project root directory where the pyproject.toml is.
Args:
repository: The name of the Python package repository, such pypi, testpypi.
token: The token to use for authentication on python package repository. If token is provided, no username/password should be provided at the same time.
username: The username to use for authentication on python package repository.
password: The password to use for authentication on python package repository.
loglevel: The log level to use.
Raises:
Exit: If arguments provided are not correct or the publish fails.
"""
console.set_log_level(loglevel)
# Validate the repository name.
repository = _validate_repository_name(repository)
console.print(f"Publishing custom component to {repository}...")
# Validate the credentials.
username, password = _validate_credentials(username, password, token)
# Validate the distribution directory.
_ensure_dist_dir()
# We install twine on the fly if required so it is not a stable dependency of reflex.
try:
import twine # noqa: F401 # type: ignore
except (ImportError, ModuleNotFoundError) as ex:
if not _pip_install_on_demand("twine"):
raise typer.Exit(code=1) from ex
publish_cmds = [
sys.executable,
"-m",
"twine",
"upload",
"--repository-url",
CustomComponents.REPO_URLS[repository],
"--username",
username,
"--password",
password,
"--non-interactive",
f"{CustomComponents.DIST_DIR}/*",
]
if _run_commands_in_subprocess(publish_cmds):
console.info("Custom component published successfully!")
else:
raise typer.Exit(1)

View File

@ -15,6 +15,7 @@ from reflex_cli.utils import dependency
from reflex import constants
from reflex.config import get_config
from reflex.custom_components.custom_components import custom_components_cli
from reflex.utils import console, telemetry
# Disable typer+rich integration for help panels
@ -580,6 +581,11 @@ cli.add_typer(
name="deployments",
help="Subcommands for managing the Deployments.",
)
cli.add_typer(
custom_components_cli,
name="component",
help="Subcommands for creating and publishing Custom Components.",
)
if __name__ == "__main__":
cli()