diff --git a/poetry.lock b/poetry.lock index 777a8329..bdc2da40 100644 --- a/poetry.lock +++ b/poetry.lock @@ -550,6 +550,21 @@ files = [ {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] +[[package]] +name = "backports-tarfile" +version = "1.2.0" +description = "Backport of CPython tarfile module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, + {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -626,6 +641,29 @@ urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version > [package.extras] crt = ["awscrt (==0.19.19)"] +[[package]] +name = "build" +version = "1.2.2.post1" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.8" +files = [ + {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, + {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +packaging = ">=19.1" +pyproject_hooks = "*" + +[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 = ["build[uv,virtualenv]", "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 = ["build[uv]", "importlib-metadata (>=5.1)", "mypy (>=1.9.0,<1.10.0)", "tomli", "typing-extensions (>=3.7.4.3)"] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.0.35)"] + [[package]] name = "bytecode" version = "0.16.0" @@ -1196,6 +1234,17 @@ idna = ["idna (>=3.7)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + [[package]] name = "ecdsa" version = "0.19.0" @@ -1855,6 +1904,25 @@ files = [ [package.dependencies] pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} +[[package]] +name = "id" +version = "1.5.0" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, + {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, +] + +[package.dependencies] +requests = "*" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "identify" version = "2.6.2" @@ -2036,6 +2104,64 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, + {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, +] + +[package.dependencies] +"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""} + +[package.extras] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, + {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["pytest-mypy"] + [[package]] name = "jedi" version = "0.19.2" @@ -2055,6 +2181,21 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + [[package]] name = "jinja2" version = "3.1.4" @@ -2460,6 +2601,35 @@ files = [ {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, ] +[[package]] +name = "keyring" +version = "25.6.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +files = [ + {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, + {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, +] + +[package.dependencies] +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"] + [[package]] name = "lark-parser" version = "0.7.8" @@ -2685,6 +2855,17 @@ files = [ {file = "monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7"}, ] +[[package]] +name = "more-itertools" +version = "10.6.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -2957,6 +3138,39 @@ files = [ {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, ] +[[package]] +name = "nh3" +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -4066,6 +4280,17 @@ docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] full = ["Pillow (>=8.0.0)", "cryptography"] image = ["Pillow (>=8.0.0)"] +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + [[package]] name = "pyreadline3" version = "3.5.4" @@ -4232,6 +4457,17 @@ files = [ {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + [[package]] name = "pywinpty" version = "2.0.14" @@ -4430,6 +4666,25 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + [[package]] name = "redis" version = "5.2.0" @@ -4629,6 +4884,20 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + [[package]] name = "rfc3986-validator" version = "0.1.1" @@ -4789,6 +5058,21 @@ botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "selenium" version = "4.26.1" @@ -5476,6 +5760,31 @@ files = [ trio = ">=0.11" wsproto = ">=0.14" +[[package]] +name = "twine" +version = "6.1.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.8" +files = [ + {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"}, + {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"}, +] + +[package.dependencies] +id = "*" +keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = ">=24.0" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=15.1)"] + [[package]] name = "typer" version = "0.9.4" @@ -6193,4 +6502,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.11,<3.12" -content-hash = "f57d0d18c10e49fd5cb243bf508dff8069b8422fcd5831cb8133184e1b52cb4e" +content-hash = "1183d0728a6986cf1585dd516163c710fe14648b2bb12928b7db7eb99c9027d6" diff --git a/pyproject.toml b/pyproject.toml index f872d1cd..d6605473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [tool.poetry] name = "skyvern" -version = "0.1.0" +version = "0.1.55" description = "" authors = ["Skyvern AI "] readme = "README.md" -packages = [{ include = "skyvern" }] +packages = [{ include = "skyvern" }, { include = "alembic" }] [tool.poetry.dependencies] python = "^3.11,<3.12" @@ -76,6 +76,8 @@ snoop = "^0.4.3" rich = {extras = ["jupyter"], version = "^13.7.0"} fpdf = "^1.7.2" pypdf = "^5.0.1" +twine = "^6.1.0" +build = "^1.2.2.post1" [build-system] @@ -129,3 +131,6 @@ skip = ["webeye/actions/__init__.py", "forge/sdk/__init__.py"] [tool.mypy] plugins = "sqlalchemy.ext.mypy.plugin" + +[tool.poetry.scripts] +skyvern = "skyvern.cli.commands:app" diff --git a/skyvern/agent/local.py b/skyvern/agent/local.py new file mode 100644 index 00000000..a0daa573 --- /dev/null +++ b/skyvern/agent/local.py @@ -0,0 +1,144 @@ +from dotenv import load_dotenv + +from skyvern.agent.parameter import TaskV1Request, TaskV2Request +from skyvern.forge import app +from skyvern.forge.sdk.core import security, skyvern_context +from skyvern.forge.sdk.core.skyvern_context import SkyvernContext +from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType +from skyvern.forge.sdk.schemas.observers import ObserverTask, ObserverTaskStatus +from skyvern.forge.sdk.schemas.organizations import Organization +from skyvern.forge.sdk.schemas.tasks import TaskResponse, TaskStatus +from skyvern.forge.sdk.services import observer_service +from skyvern.forge.sdk.services.org_auth_token_service import API_KEY_LIFETIME +from skyvern.forge.sdk.workflow.models.workflow import WorkflowRunStatus +from skyvern.utils import migrate_db + + +class Agent: + def __init__(self) -> None: + load_dotenv(".env") + migrate_db() + + async def _get_organization(self) -> Organization: + organization = await app.DATABASE.get_organization_by_domain("skyvern.local") + if not organization: + organization = await app.DATABASE.create_organization( + organization_name="Skyvern-local", + domain="skyvern.local", + max_steps_per_run=10, + max_retries_per_step=3, + ) + api_key = security.create_access_token( + organization.organization_id, + expires_delta=API_KEY_LIFETIME, + ) + # generate OrganizationAutoToken + await app.DATABASE.create_org_auth_token( + organization_id=organization.organization_id, + token=api_key, + token_type=OrganizationAuthTokenType.api, + ) + return organization + + async def run_task_v1(self, task_request: TaskV1Request) -> TaskResponse: + organization = await self._get_organization() + + org_auth_token = await app.DATABASE.get_valid_org_auth_token( + organization_id=organization.organization_id, + token_type=OrganizationAuthTokenType.api, + ) + + created_task = await app.agent.create_task(task_request, organization.organization_id) + + skyvern_context.set( + SkyvernContext( + organization_id=organization.organization_id, + task_id=created_task.task_id, + max_steps_override=task_request.max_steps, + ) + ) + + step = await app.DATABASE.create_step( + created_task.task_id, + order=0, + retry_index=0, + organization_id=organization.organization_id, + ) + updated_task = await app.DATABASE.update_task( + created_task.task_id, + status=TaskStatus.running, + organization_id=organization.organization_id, + ) + + step, _, _ = await app.agent.execute_step( + organization=organization, + task=updated_task, + step=step, + api_key=org_auth_token.token if org_auth_token else None, + ) + + refreshed_task = await app.DATABASE.get_task(created_task.task_id, organization.organization_id) + if refreshed_task: + updated_task = refreshed_task + + failure_reason: str | None = None + if updated_task.status == TaskStatus.failed and (step.output or updated_task.failure_reason): + failure_reason = "" + if updated_task.failure_reason: + failure_reason += updated_task.failure_reason or "" + if step.output is not None and step.output.actions_and_results is not None: + action_results_string: list[str] = [] + for action, results in step.output.actions_and_results: + if len(results) == 0: + continue + if results[-1].success: + continue + action_results_string.append(f"{action.action_type} action failed.") + + if len(action_results_string) > 0: + failure_reason += "(Exceptions: " + str(action_results_string) + ")" + return await app.agent.build_task_response( + task=updated_task, last_step=step, failure_reason=failure_reason, need_browser_log=True + ) + + async def run_task_v2(self, task_request: TaskV2Request) -> ObserverTask: + organization = await self._get_organization() + + observer_task = await observer_service.initialize_observer_task( + organization=organization, + user_prompt=task_request.user_prompt, + user_url=str(task_request.url) if task_request.url else None, + totp_identifier=task_request.totp_identifier, + totp_verification_url=task_request.totp_verification_url, + webhook_callback_url=task_request.webhook_callback_url, + proxy_location=task_request.proxy_location, + publish_workflow=task_request.publish_workflow, + ) + + if not observer_task.workflow_run_id: + raise Exception("Observer cruise missing workflow run id") + + # mark observer cruise as queued + await app.DATABASE.update_observer_cruise( + observer_cruise_id=observer_task.observer_cruise_id, + status=ObserverTaskStatus.queued, + organization_id=organization.organization_id, + ) + await app.DATABASE.update_workflow_run( + workflow_run_id=observer_task.workflow_run_id, + status=WorkflowRunStatus.queued, + ) + + await observer_service.run_observer_task( + organization=organization, + observer_cruise_id=observer_task.observer_cruise_id, + max_iterations_override=task_request.max_iterations, + ) + + refreshed_observer_task = await app.DATABASE.get_observer_cruise( + observer_cruise_id=observer_task.observer_cruise_id, organization_id=organization.organization_id + ) + if refreshed_observer_task: + return refreshed_observer_task + + return observer_task diff --git a/skyvern/agent/parameter.py b/skyvern/agent/parameter.py new file mode 100644 index 00000000..1456897b --- /dev/null +++ b/skyvern/agent/parameter.py @@ -0,0 +1,10 @@ +from skyvern.forge.sdk.schemas.observers import ObserverTaskRequest +from skyvern.forge.sdk.schemas.tasks import TaskRequest + + +class TaskV1Request(TaskRequest): + max_steps: int = 10 + + +class TaskV2Request(ObserverTaskRequest): + max_iterations: int = 10 diff --git a/skyvern/agent/remote.py b/skyvern/agent/remote.py new file mode 100644 index 00000000..7e61ad09 --- /dev/null +++ b/skyvern/agent/remote.py @@ -0,0 +1,43 @@ +import httpx + +from skyvern.agent.parameter import TaskV1Request, TaskV2Request +from skyvern.forge.sdk.schemas.observers import ObserverTask +from skyvern.forge.sdk.schemas.tasks import CreateTaskResponse, TaskResponse + + +class RemoteAgent: + def __init__(self, api_key: str, endpoint: str = "https://api.skyvern.com"): + self.endpoint = endpoint + self.api_key = api_key + self.client = httpx.AsyncClient( + headers={ + "Content-Type": "application/json", + "x-api-key": self.api_key, + } + ) + + async def run_task_v1(self, task: TaskV1Request) -> CreateTaskResponse: + url = f"{self.endpoint}/api/v1/tasks" + payload = task.model_dump_json() + headers = {"x_max_steps_override": str(task.max_steps)} + response = await self.client.post(url, headers=headers, data=payload) + return CreateTaskResponse.model_validate(response.json()) + + async def run_task_v2(self, task: TaskV2Request) -> ObserverTask: + url = f"{self.endpoint}/api/v2/tasks" + payload = task.model_dump_json() + headers = {"x_max_iterations_override": str(task.max_iterations)} + response = await self.client.post(url, headers=headers, data=payload) + return ObserverTask.model_validate(response.json()) + + async def get_task_v1(self, task_id: str) -> TaskResponse: + """Get a task by id.""" + url = f"{self.endpoint}/api/v1/tasks/{task_id}" + response = await self.client.get(url) + return TaskResponse.model_validate(response.json()) + + async def get_task_v2(self, task_id: str) -> ObserverTask: + """Get a task by id.""" + url = f"{self.endpoint}/api/v2/tasks/{task_id}" + response = await self.client.get(url) + return ObserverTask.model_validate(response.json()) diff --git a/skyvern/cli/commands.py b/skyvern/cli/commands.py new file mode 100644 index 00000000..e3684ea2 --- /dev/null +++ b/skyvern/cli/commands.py @@ -0,0 +1,126 @@ +import shutil +import subprocess +import time +from typing import Optional + +import typer + +from skyvern.utils import migrate_db + +app = typer.Typer() + + +def command_exists(command: str) -> bool: + return shutil.which(command) is not None + + +def run_command(command: str, check: bool = True) -> tuple[Optional[str], Optional[int]]: + try: + result = subprocess.run(command, shell=True, check=check, capture_output=True, text=True) + return result.stdout.strip(), result.returncode + except subprocess.CalledProcessError as e: + return None, e.returncode + + +def is_postgres_running() -> bool: + if command_exists("pg_isready"): + result = run_command("pg_isready") + return result is not None and "accepting connections" in result + return False + + +def database_exists(dbname: str, user: str) -> bool: + check_db_command = f'psql {dbname} -U {user} -c "\\q"' + return run_command(check_db_command, check=False) is not None + + +def create_database_and_user() -> None: + print("Creating database user and database...") + run_command("createuser skyvern") + run_command("createdb skyvern -O skyvern") + print("Database and user created successfully.") + + +def is_docker_running() -> bool: + if not command_exists("docker"): + return False + _, code = run_command("docker info", check=False) + return code == 0 + + +def is_postgres_running_in_docker() -> bool: + _, code = run_command("docker ps | grep -q postgresql-container", check=False) + return code == 0 + + +def is_postgres_container_exists() -> bool: + _, code = run_command("docker ps -a | grep -q postgresql-container", check=False) + return code == 0 + + +def setup_postgresql() -> None: + print("Setting up PostgreSQL...") + + if command_exists("psql") and is_postgres_running(): + print("PostgreSQL is already running locally.") + if database_exists("skyvern", "skyvern"): + print("Database and user exist.") + else: + create_database_and_user() + return + + if not is_docker_running(): + print("Docker is not running or not installed. Please install or start Docker and try again.") + exit(1) + + if is_postgres_running_in_docker(): + print("PostgreSQL is already running in a Docker container.") + else: + print("Attempting to install PostgreSQL via Docker...") + if not is_postgres_container_exists(): + run_command( + "docker run --name postgresql-container -e POSTGRES_HOST_AUTH_METHOD=trust -d -p 5432:5432 postgres:14" + ) + else: + run_command("docker start postgresql-container") + print("PostgreSQL has been installed and started using Docker.") + + print("Waiting for PostgreSQL to start...") + time.sleep(20) + + _, code = run_command('docker exec postgresql-container psql -U postgres -c "\\du" | grep -q skyvern', check=False) + if code == 0: + print("Database user exists.") + else: + print("Creating database user...") + run_command("docker exec postgresql-container createuser -U postgres skyvern") + + _, code = run_command( + "docker exec postgresql-container psql -U postgres -lqt | cut -d \\| -f 1 | grep -qw skyvern", check=False + ) + if code == 0: + print("Database exists.") + else: + print("Creating database...") + run_command("docker exec postgresql-container createdb -U postgres skyvern -O skyvern") + print("Database and user created successfully.") + + +@app.command(name="init") +def init( + openai_api_key: str = typer.Option(..., help="The OpenAI API key"), + log_level: str = typer.Option("CRITICAL", help="The log level"), +) -> None: + setup_postgresql() + # Generate .env file + with open(".env", "w") as env_file: + env_file.write("ENABLE_OPENAI=true\n") + env_file.write(f"OPENAI_API_KEY={openai_api_key}\n") + env_file.write(f"LOG_LEVEL={log_level}\n") + env_file.write("ARTIFACT_STORAGE_PATH=./artifacts\n") + print(".env file created with the parameters provided.") + + +@app.command(name="migrate") +def migrate() -> None: + migrate_db() diff --git a/skyvern/config.py b/skyvern/config.py index f3a5c759..ebb171c6 100644 --- a/skyvern/config.py +++ b/skyvern/config.py @@ -11,7 +11,7 @@ class Settings(BaseSettings): BROWSER_TYPE: str = "chromium-headful" MAX_SCRAPING_RETRIES: int = 0 - VIDEO_PATH: str | None = None + VIDEO_PATH: str | None = "./video" HAR_PATH: str | None = "./har" LOG_PATH: str = "./log" TEMP_PATH: str = "./temp" diff --git a/skyvern/utils/__init__.py b/skyvern/utils/__init__.py new file mode 100644 index 00000000..db088fc9 --- /dev/null +++ b/skyvern/utils/__init__.py @@ -0,0 +1,10 @@ +from alembic import command +from alembic.config import Config +from skyvern.constants import REPO_ROOT_DIR + + +def migrate_db() -> None: + alembic_cfg = Config() + path = f"{REPO_ROOT_DIR}/alembic" + alembic_cfg.set_main_option("script_location", path) + command.upgrade(alembic_cfg, "head")