From b4a4401b8bbf3bfca1b5cb1f7f648ea676b7f568 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Sun, 24 Jul 2022 23:51:12 +0530 Subject: [PATCH 1/5] Make pydantic based config system Functions that work with the new system but have backwards compatible signatures were made in the new config.py file; functions that weren't being used anywhere were not replicated. The old script.py can be deleted once all references to it has been safely replaced. --- etc/smashbox.template.yaml | 116 ++++++++++ poetry.lock | 423 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 20 ++ python/smashbox/__init__.py | 1 + python/smashbox/config.py | 99 +++++++++ python/smashbox/script.py | 1 - python/smashbox/webdav.py | 230 ++++++++++++++++++++ 7 files changed, 889 insertions(+), 1 deletion(-) create mode 100644 etc/smashbox.template.yaml create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 python/smashbox/config.py create mode 100644 python/smashbox/webdav.py diff --git a/etc/smashbox.template.yaml b/etc/smashbox.template.yaml new file mode 100644 index 00000000..21fc3aa9 --- /dev/null +++ b/etc/smashbox.template.yaml @@ -0,0 +1,116 @@ +# top directory where all local working files are kept (test working direcotires, test logs etc) +smashdir: "~/smashdir" + +# name of the account used for testing; automatically picked if null +oc_account_name: null + +# default number of users for tests involving multiple users (user number is appended to the oc_account_name) +# this only applies to the tests involving multiple users +oc_number_test_users: 3 + +# name of the group used for testing +oc_group_name: null + +# default number of groups for tests involving multiple groups (group number is appended to the oc_group_name) +# this only applies to the tests involving multiple groups +oc_number_test_groups: 1 + +# password for test accounts: all test account will have the same password +# if not set then it's an error +oc_account_password: "" + +# owncloud test server +# if left blank or "localhost" then the real hostname of the localhost will be set +oc_server: "" + +# root of the owncloud installation as visible in the URL +oc_root: "owncloud" + +# oc_webdav_endpoint will be computed based on oc_root + +# target folder on the server (this may not be compatible with all tests) +oc_server_folder: "" + +# should we use protocols with SSL (https, ownclouds) +oc_ssl_enabled: true + +# how to invoke shell commands on the server +# for localhost there is no problem - leave it blank +# for remote host it may be set like this: "ssh -t -l root $oc_server" +# note: configure ssh for passwordless login +# note: -t option is to make it possible to run sudo +oc_server_shell_cmd: "" + +# Data directory on the owncloud server. +# computed based on oc_root as /var/www/html + oc_root + data + +# a path to server side tools (create_user.php, ...) +# it may be specified as relative path "dir" and then resolves to +# /dir where is the top-level of of the tree +# containing THIS configuration file +oc_server_tools_path: "server-tools" + +# a path to ocsync command with options +# this path should work for all client hosts +# +# it may be specified as relative path "./dir" and then resolves to +# /dir where is the top-level of of the tree +# containing THIS configuration file, e.g.: "./client/build/mirall/bin/owncloudcmd --trust" +# +# it may be specified as a basename executable (PATH will be used), e.g. "cernboxcmd --trust" +# +# it may be specified as absolute executable path, e.g. "/usr/bin/cernboxcmd --trust" +# +oc_sync_cmd: "cernboxcmd" + +# number of times to repeat ocsync run every time +oc_sync_repeat: 1 + +########################################### + +# unique identifier of your test run +# if null then the runid is chosen automatically +runid: null + +# if True then the local working directory path will have the runid added to it automatically +workdir_runid_enabled: false + +# if True then the runid will be part of the oc_account_name automatically +oc_account_runid_enabled: false + +#################################### + +# this defines the default account cleanup procedure +# - "delete": delete account if exists and then create a new account with the same name +# - "keep": don't delete existing account but create one if needed +# +# these are not implemeted yet: +# - "sync_delete": delete all files via a sync run +# - "webdav_delete": delete all files via webdav DELETE request +# - "filesystem_delete": delete all files directly on the server's filesystem +oc_account_reset_procedure: "delete" + +# this defined the default local run directory reset procedure +# - "delete": delete everything in the local run directory prior to running the test +# - "keep": keep all files (from the previous run) +rundir_reset_procedure: "delete" + +web_user: "www-data" + +oc_admin_user: "at_admin" +oc_admin_password: "admin" + +# Verbosity of curl client. +# If none then verbosity is on when smashbox run in --debug mode. +# set it to True or False to override +# This setting is no longer needed as pycurl isn't used +# pycurl_verbose: null + +# scp port to be used in scp commands, used primarily when copying over the server log file +scp_port: 22 + +# user that can r+w the owncloud.log file (needs to be configured for passwordless login) +oc_server_log_user: "www-data" + +# Reset the server log file and verify that no exceptions and other known errors have been logged +oc_check_server_log: false diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..68d4dc2a --- /dev/null +++ b/poetry.lock @@ -0,0 +1,423 @@ +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.15.0" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.16.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pydantic" +version = "1.9.1" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "e3392be6cf4d86dd2184ea6b7ab83d6e63ebdf93c4c708c0c45939268ef8ca96" + +[metadata.files] +anyio = [ + {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, + {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +httpcore = [ + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, +] +httpx = [ + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pydantic = [] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pyyaml = [] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typing-extensions = [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..2fb44918 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "smashbox" +version = "0.1.0" +description = "Main test suite for CERNBox" +authors = ["Your Name "] +license = "AGPL-3.0" + +[tool.poetry.dependencies] +python = "^3.10" +pytest = "^7.1.2" +httpx = "^0.23.0" +pydantic = "^1.9.1" +PyYAML = "^6.0" + +[tool.poetry.dev-dependencies] +black = "^22.3.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/python/smashbox/__init__.py b/python/smashbox/__init__.py index e69de29b..efcef1fc 100644 --- a/python/smashbox/__init__.py +++ b/python/smashbox/__init__.py @@ -0,0 +1 @@ +from . import webdav as webdav diff --git a/python/smashbox/config.py b/python/smashbox/config.py new file mode 100644 index 00000000..3d83b0c3 --- /dev/null +++ b/python/smashbox/config.py @@ -0,0 +1,99 @@ +import logging +import os.path +import pickle +from pathlib import Path +from typing import Any, Literal + +import yaml +from pydantic import BaseSettings + + +logger = None + +# this should probably be moved into a utilities module +def get_logger(name: str = "config", level: int | None = None) -> logging.Logger: + global logger + if not logger: + if level is None: + level = logging.INFO # change here to DEBUG if you want to debug config stuff + logging.basicConfig(level=level) + return logging.getLogger('.'.join(['smash', name])) + + +class Configuration(BaseSettings): + """Root configuration object that parses the values defined in the config file.""" + smashdir: str + oc_account_name: str | None + oc_number_test_users: int + oc_group_name: str | None + oc_number_test_groups: int + oc_account_password: str + oc_server: str + oc_root: str + oc_server_folder: str + oc_ssl_enabled: bool + oc_server_shell_cmd: str + oc_server_tools_path: str # TODO: is this still needed? + oc_sync_cmd: str + oc_sync_repeat: int + + runid: int | None + workdir_runid_enabled: bool + oc_account_runid_enabled: bool + + oc_account_reset_procedure: Literal["delete", "keep"] # there are some more types that are not yet implemented + rundir_reset_procedure: Literal["delete", "keep"] + + web_user: str + oc_admin_user: str + oc_admin_password: str + + scp_port: int + oc_server_log_user: str + oc_check_server_log: bool + + @property + def oc_webdav_endpoint(self) -> str: + return os.path.join(self.oc_root, "remote.php/webdav") + + @property + def oc_server_datadirectory(self) -> str: + return os.path.join("/var/www/html", self.oc_root, "data") + + # these methods exists for backwards compatibility + def _dict(self, **args: Any) -> dict[str, object]: + """Returns a dictionary representation of the configuration object. + Any extra arguments passed are also returned in this dictionary.""" + return {**self.dict(), **args} + + def get(self, key: str, default: object) -> object: + """Returns the value of the specified setting, or the + default if the key doesn't exist.""" + logger = get_logger() + logger.debug("config.get(%s,default=%s)", key, default) + return self._dict().get(key, default=default) + + +def log_config(config: Configuration, level: int | None = None, hide_password: bool = False) -> None: + """Dump the entire configuration to the logging system at the given level. + If hide_password=True then do not show the real value of the options which contain "password" in their names. + """ + logger = get_logger() + for key, val in config.dict().items(): + if hide_password and "password" in key: + val = "***" + logger.log(level, "CONFIG: %s = %s", key, val) + + +def load_config(fp: Path | str) -> Configuration: + """Loads and parses the specified configuration file.""" + with open(fp, "r") as file: + return Configuration(**yaml.load(file)) + +def configure_from_blob(fp: Path | str) -> Configuration: + return pickle.load(fp) + +def dump_config(config: Configuration, fp: Path | str) -> None: + """Serialize given config object as YAML and write it to the specified file.""" + with open(fp, "w") as file: + yaml.dump(config.dict(), file) diff --git a/python/smashbox/script.py b/python/smashbox/script.py index e1836edb..0a783fea 100644 --- a/python/smashbox/script.py +++ b/python/smashbox/script.py @@ -1,4 +1,3 @@ - import smashbox.compatibility.argparse as argparse def keyval_tuple(x): diff --git a/python/smashbox/webdav.py b/python/smashbox/webdav.py new file mode 100644 index 00000000..2a6b5694 --- /dev/null +++ b/python/smashbox/webdav.py @@ -0,0 +1,230 @@ +from typing import Literal, Optional +from xml.etree import ElementTree + +import httpx + +from smashbox.utilities import * +from smashbox.config import get_logger + + +logger = get_logger() +RequestType = Literal["MOVE", "PROPFIND", "PUT", "GET", "MKCOL", "DELTETE"] +PropfindDepth = Literal["1", "0", "infinity"] +OverwriteType = Literal["T", "F"] + + +class Client: + + extra_headers = {} + + def __init__(self, verbose: Optional[bool] = None) -> None: + self._client = httpx.Client(timeout=60) + self.verbose = bool(verbose) + + if config.get("pycurl_USERAGENT", None): + self._client.headers.update({"User-Agent": config.pycurl_USERAGENT}) + + def PROPFIND( + self, + url: str, + query: str, + depth: PropfindDepth, + parse_check: bool = True, + headers: Optional[dict] = None, + ) -> httpx.Response: + logger.info("PROPFIND %s depth=%s %s query=%s", url, depth, headers, query) + + if headers is None: + headers = {} + headers["Depth"] = depth + + r = self._perform_request("PROPFIND", url, content=query, headers=headers) + + if self.verbose: + logger.info("PROPFIND response body: %s", r.text) + + if parse_check: + if ( + 200 <= r.status_code and r.status_code < 300 + ): # only parse the reponse type for positive responses + # TODO: multiple Content-Type response headers will confuse the client as well + fatal_check( + "application/xml; charset=utf-8" in r.headers["Content-Type"], + 'Wrong response header "Content-Type:%s"' + % r.headers["Content-Type"], + ) # as of client 1.7 and 1.8 + r.propfind_response = _parse_propfind_response( + r.response_body, depth=depth + ) + + return r + + def PUT( + self, fn: str, url: str, headers: Optional[dict] = None, offset: int = 0 + ) -> httpx.Response: + logger.debug("PUT %s %s %s", fn, url, headers) + + with open(fn, "rb") as f: + if offset: + f.seek(offset) + r = self._perform_request("PUT", url, files={"upload-file": f}) + + if self.verbose: + logger.info("PUT response body: %s", r.text) + + return r + + def GET( + self, url: str, fn: Optional[str] = None, headers: Optional[dict] = None + ) -> httpx.Response: + logger.debug("GET %s %s %s", url, fn, headers) + + if fn: + f = open(fn, "wb") + + r = self._perform_request("GET", url, headers=headers) + + if fn: + f.write(r.read()) + f.close() + else: + r.response_body = r.read() + + return r + + def MKCOL(self, url: str) -> httpx.Response: + logger.debug("MKCOL %s", url) + return self._perform_request("MKCOL", url) + + def DELETE(self, url: str) -> httpx.Response: + logger.debug("DELETE %s", url) + return self._perform_request("DELETE", url) + + def MOVE( + self, url: str, destination: str, overwrite: Optional[OverwriteType] = None + ) -> httpx.Response: + logger.debug("MOVE %s %s %s", url, destination, overwrite) + headers = {} + headers["Destination"] = destination + if overwrite: + headers["Overwrite"] = overwrite + return self._perform_request("MOVE", url, headers=headers) + + def _perform_request( + self, + method: RequestType, + url: str, + content: Optional[str] = None, + files: Optional[dict] = None, + headers: Optional[httpx._types.HeaderTypes] = None, + ) -> httpx.Response: + _headers = self.extra_headers.copy() + _headers.update(headers) + + res = self._client.request( + method, url, content=content, headers=_headers, files=files + ) + logger.debug( + "_perform_request url=%s header=%s rc=%s reply_headers=%s", + url, + headers, + res.status_code, + res.headers, + ) + + return res + + +def _parse_propfind_response(text: str, depth: Optional[PropfindDepth] = None) -> list: + """Basic parsing and validation of PROPFIND responses. + + If depth is defined add validation according to the depth. + """ + + def allowed_children_tags(e: ElementTree.Element, tags: list[str]) -> None: + for c in e: + fatal_check(c.tag in tags) + + logger.debug("xml.etree.ElementTree parsing: %s", text) + root = ElementTree.fromstring(text) + + fatal_check(root.tag == "{DAV:}multistatus") + + num_responses = len(root.findall("{DAV:}response")) + + fatal_check(num_responses > 0) + + if depth is not None: + if str(depth) == "0": + fatal_check(num_responses == 1) + else: + fatal_check(num_responses >= 1) + + allowed_children_tags(root, ["{DAV:}response"]) # only this element is allowed + + responses = [] + + for child in root: + + allowed_children_tags( + child, ["{DAV:}href", "{DAV:}propstat"] + ) # only these elements are allowed + + fatal_check( + len(child.findall("{DAV:}href")) == 1 + ) # exactly one href per response + href = child.find("{DAV:}href") + + r = [href.text, {}] + + for propstat in child.findall("{DAV:}propstat"): + + logger.debug("xml.etree.ElementTree parsing: %s", propstat.tag) + + allowed_children_tags(propstat, ["{DAV:}prop", "{DAV:}status"]) + fatal_check(len(propstat.findall("{DAV:}prop")) == 1) # exactly one + fatal_check(len(propstat.findall("{DAV:}status")) == 1) # exactly one + + status = propstat.find("{DAV:}status") + prop = propstat.find("{DAV:}prop") + + rp = {} + + for p in prop: + logger.debug("xml.etree.ElementTree parsing: %s", p.tag) + fatal_check( + not rp.has_key(p.tag), p.tag + ) # duplicate elements are not allowed + if p.tag == "{DAV:}resourcetype": + fatal_check( + len(list(p)) in [0, 1] + ) # either empty or exactly one element + try: + resource_type = list(p)[0] + # For convenience we include some owncloud-specific input validation here. It should be removed in the future. + # In the response body the value of resourcetype property MUST NOT contain whitespaces (implementation as of owncloud client 1.5): + # + fatal_check(resource_type.tag == "{DAV:}collection") + fatal_check( + len(list(resource_type)) == 0 + ) # must have no children + fatal_check( + not p.text + ) # there must not be text in front of , FIXME: this may be relaxed later to allow whitespaces + fatal_check( + not resource_type.text + ) # there must not be text inside + fatal_check( + not resource_type.tail + ) # there must not be text behind , FIXME: this may be relaxed later to allow whitespaces + rp[p.tag] = "{DAV:}collection" + except IndexError: + rp[p.tag] = None + else: + rp[p.tag] = p.text + + r[1][status.text] = rp + + responses.append(r) + + return responses From 079128ad0cbd4fe4fd2aa354183ff7ae5dffa09a Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 29 Jul 2022 15:04:44 +0530 Subject: [PATCH 2/5] Reformat with black --- python/smashbox/config.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/python/smashbox/config.py b/python/smashbox/config.py index 3d83b0c3..cfcb27c3 100644 --- a/python/smashbox/config.py +++ b/python/smashbox/config.py @@ -14,14 +14,17 @@ def get_logger(name: str = "config", level: int | None = None) -> logging.Logger: global logger if not logger: - if level is None: - level = logging.INFO # change here to DEBUG if you want to debug config stuff - logging.basicConfig(level=level) - return logging.getLogger('.'.join(['smash', name])) + if level is None: + level = ( + logging.INFO + ) # change here to DEBUG if you want to debug config stuff + logging.basicConfig(level=level) + return logging.getLogger(".".join(["smash", name])) class Configuration(BaseSettings): """Root configuration object that parses the values defined in the config file.""" + smashdir: str oc_account_name: str | None oc_number_test_users: int @@ -41,7 +44,9 @@ class Configuration(BaseSettings): workdir_runid_enabled: bool oc_account_runid_enabled: bool - oc_account_reset_procedure: Literal["delete", "keep"] # there are some more types that are not yet implemented + oc_account_reset_procedure: Literal[ + "delete", "keep" + ] # there are some more types that are not yet implemented rundir_reset_procedure: Literal["delete", "keep"] web_user: str @@ -59,13 +64,13 @@ def oc_webdav_endpoint(self) -> str: @property def oc_server_datadirectory(self) -> str: return os.path.join("/var/www/html", self.oc_root, "data") - + # these methods exists for backwards compatibility def _dict(self, **args: Any) -> dict[str, object]: """Returns a dictionary representation of the configuration object. Any extra arguments passed are also returned in this dictionary.""" return {**self.dict(), **args} - + def get(self, key: str, default: object) -> object: """Returns the value of the specified setting, or the default if the key doesn't exist.""" @@ -74,7 +79,9 @@ def get(self, key: str, default: object) -> object: return self._dict().get(key, default=default) -def log_config(config: Configuration, level: int | None = None, hide_password: bool = False) -> None: +def log_config( + config: Configuration, level: int | None = None, hide_password: bool = False +) -> None: """Dump the entire configuration to the logging system at the given level. If hide_password=True then do not show the real value of the options which contain "password" in their names. """ @@ -90,9 +97,11 @@ def load_config(fp: Path | str) -> Configuration: with open(fp, "r") as file: return Configuration(**yaml.load(file)) + def configure_from_blob(fp: Path | str) -> Configuration: return pickle.load(fp) + def dump_config(config: Configuration, fp: Path | str) -> None: """Serialize given config object as YAML and write it to the specified file.""" with open(fp, "w") as file: From 21ec9ea385efc3173323944ca37ecbe486d4c655 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:04:37 +0530 Subject: [PATCH 3/5] Complete basic loguru setup All logging calls have to be slowly converted after this. --- poetry.lock | 30 +++++- pyproject.toml | 1 + python/smashbox/config.py | 29 ++--- python/smashbox/script.py | 220 ++++++++++++++++++-------------------- 4 files changed, 145 insertions(+), 135 deletions(-) diff --git a/poetry.lock b/poetry.lock index 68d4dc2a..1661db49 100644 --- a/poetry.lock +++ b/poetry.lock @@ -147,6 +147,21 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["sphinx-rtd-theme (>=0.4.3)", "sphinx-autobuild (>=0.7.1)", "Sphinx (>=4.1.1)", "isort (>=5.1.1)", "black (>=19.10b0)", "pytest-cov (>=2.7.1)", "pytest (>=4.6.2)", "tox (>=3.9.0)", "flake8 (>=3.7.7)", "docutils (==0.16)", "colorama (>=0.3.4)"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -299,10 +314,21 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "e3392be6cf4d86dd2184ea6b7ab83d6e63ebdf93c4c708c0c45939268ef8ca96" +content-hash = "ed95f858055b4ddd0367fee27b6a6f4567edbace8ed1d73d09c2c3193c26019d" [metadata.files] anyio = [ @@ -374,6 +400,7 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +loguru = [] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -421,3 +448,4 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [] +win32-setctime = [] diff --git a/pyproject.toml b/pyproject.toml index a9a62413..a4994bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ pytest = "^7.1.2" httpx = "^0.23.0" pydantic = "^1.9.1" PyYAML = "^6.0" +loguru = "^0.6.0" [tool.poetry.dev-dependencies] black = "^22.3.0" diff --git a/python/smashbox/config.py b/python/smashbox/config.py index cfcb27c3..d0d821a6 100644 --- a/python/smashbox/config.py +++ b/python/smashbox/config.py @@ -1,25 +1,15 @@ -import logging import os.path import pickle +from functools import cache from pathlib import Path from typing import Any, Literal import yaml +from loguru import logger from pydantic import BaseSettings -logger = None - -# this should probably be moved into a utilities module -def get_logger(name: str = "config", level: int | None = None) -> logging.Logger: - global logger - if not logger: - if level is None: - level = ( - logging.INFO - ) # change here to DEBUG if you want to debug config stuff - logging.basicConfig(level=level) - return logging.getLogger(".".join(["smash", name])) +main_config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'etc', 'smashbox.conf.yaml') class Configuration(BaseSettings): @@ -74,8 +64,7 @@ def _dict(self, **args: Any) -> dict[str, object]: def get(self, key: str, default: object) -> object: """Returns the value of the specified setting, or the default if the key doesn't exist.""" - logger = get_logger() - logger.debug("config.get(%s,default=%s)", key, default) + logger.debug("config.get({} ,default={})", key, default) return self._dict().get(key, default=default) @@ -85,15 +74,15 @@ def log_config( """Dump the entire configuration to the logging system at the given level. If hide_password=True then do not show the real value of the options which contain "password" in their names. """ - logger = get_logger() for key, val in config.dict().items(): if hide_password and "password" in key: val = "***" - logger.log(level, "CONFIG: %s = %s", key, val) - + logger.log(level, "CONFIG: {} = {}", key, val) -def load_config(fp: Path | str) -> Configuration: - """Loads and parses the specified configuration file.""" +@cache() +def load_config(fp: Path | str = main_config_file) -> Configuration: + """Loads and parses the specified configuration file. + This function call is cached, that is it will return the same object for a given file path.""" with open(fp, "r") as file: return Configuration(**yaml.load(file)) diff --git a/python/smashbox/script.py b/python/smashbox/script.py index 0a783fea..4bdc3355 100644 --- a/python/smashbox/script.py +++ b/python/smashbox/script.py @@ -1,129 +1,121 @@ -import smashbox.compatibility.argparse as argparse +import logging +import os.path +from pathlib import Path +from typing import List, Sequence, Tuple -def keyval_tuple(x): - a,b = x.split('=',1) - return (a.strip(),b) +from loguru import logger -def arg_parser(**kwds): - """ Create an ArgumentParser with common options for smash scripts and tools. - """ - - parser = argparse.ArgumentParser(**kwds) - - parser.add_argument('--option', '-o', metavar="key=val", dest="options", type=keyval_tuple, action='append', help='set config option') - parser.add_argument('--dry-run', '-n', action='store_true', help='show config options and print what tests would be run') - parser.add_argument('--quiet', '-q', action="store_true", help='do not produce output (other than errors)') - parser.add_argument('--verbose', '-v', action="store_true", help='produce more output') - parser.add_argument('--debug', action="store_true", help='produce very verbose output') - parser.add_argument('--config','-c',dest="configs",default=[],action="append",help='config files (one or more), added on to of default config file') - return parser +import smashbox.compatibility.argparse as argparse +from smashbox.config import load_config, log_config -import os.path -main_config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),'etc','smashbox.conf') +main_config_file = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "etc", + "smashbox.conf.yaml", +) -class Configuration: - # you may use config object for string interpolation "..."%config - def __getitem__(self,x): - return getattr(self,x) - def _dict(self,**args): - return dict(self.__dict__.items() + args.items()) +def keyval_tuple(x: str) -> Tuple[str, str]: + a, b = x.split("=", 1) + return (a.strip(), b) - def get(self,x,default): - logger = getLogger() - logger.debug('config.get(%s,default=%s)',repr(x),repr(default)) - try: - return getattr(self,x) - except AttributeError: - return default +def arg_parser(**kwds): + """Create an ArgumentParser with common options for smash scripts and tools.""" -config = Configuration() + parser = argparse.ArgumentParser(**kwds) -def configure_from_blob(config_blob): - import pickle - global config - config = pickle.loads(config_blob) - config_log(level=logging.DEBUG) - return config + parser.add_argument( + "--option", + "-o", + metavar="key=val", + dest="options", + type=keyval_tuple, + action="append", + help="set config option", + ) + parser.add_argument( + "--dry-run", + "-n", + action="store_true", + help="show config options and print what tests would be run", + ) + parser.add_argument( + "--quiet", + "-q", + action="store_true", + help="do not produce output (other than errors)", + ) + parser.add_argument( + "--verbose", "-v", action="store_true", help="produce more output" + ) + parser.add_argument( + "--debug", action="store_true", help="produce very verbose output" + ) + parser.add_argument( + "--config", + "-c", + dest="configs", + default=[], + action="append", + help="config files (one or more), added on to of default config file", + ) + return parser -def dump_config_to_blob(): - import pickle - return pickle.dumps(config) - -def configure(cmdline_opts,config_files=None): - """ Initialize config object and return it. - - First exec the sequence of config_files (including the - main_config_file). All symbols defined by these files will be set - as attributes of the config object. - - Then process cmdline_opts (which is a list of tuples generated by - arg_parser). If attribute matching the option already exists (was - defined in a configuration file) then eval to the same type (if not - None). Otherwise leave string values. The string "None" is special - and it is always converted to None and may always be assigned. - - """ - - if config_files is None: - config_files = [] - - logger = getLogger() - - config_files = [main_config_file] + config_files - - for cf in config_files: - execfile(cf,{},config.__dict__) - - if cmdline_opts: - for key,val in cmdline_opts: - try: - if val == "None": - val = None - else: - attr = getattr(config,key) - # coerce val type to attr's type unless attr is set to None (then leave as-is ) - try: - if attr is not None: - val = type(attr)(val) - except ValueError,x: - # allow setting to None - logger.warning("cannot set option (type mismatch) %s=%s --> %s",key,repr(val),x) - continue - except AttributeError: - # if attr unknown then leave the val as-is (string) - pass - - setattr(config,key,val) - - config_log(level=logging.DEBUG) - - return config - -def config_log(level,hide_password=False): - """ Dump the entire configuration to the logging system at the given level. - If hide_password=True then do not show the real value of the options which contain "password" in their names. - """ - logger = getLogger() - for d in dir(config): - if not d.startswith("_") and d != "get": - value = repr(getattr(config,d)) - if hide_password and 'password' in d: - value = "***" - - logger.log(level,"CONFIG: %s = %s",d,value) +def configure( + cmdline_opts: Sequence[Tuple[str, str]], + config_files: List[Path | str] | None = None, +): + """Initialize config object and return it. -import logging + First exec the sequence of config_files (including the + main_config_file). All symbols defined by these files will be set + as attributes of the config object. + + Then process cmdline_opts (which is a list of tuples generated by + arg_parser). If attribute matching the option already exists (was + defined in a configuration file) then eval to the same type (if not + None). Otherwise leave string values. The string "None" is special + and it is always converted to None and may always be assigned. -logger = None -def getLogger(name="",level=None): - global logger - if not logger: - if level is None: - level = logging.INFO # change here to DEBUG if you want to debug config stuff - logging.basicConfig(level=level) + """ + config = load_config() + if config_files is None: + config_files = [] + + config_files = [main_config_file] + config_files + + # for cf in config_files: + # execfile(cf, {}, config.__dict__) + + if cmdline_opts: + for key, val in cmdline_opts: + try: + if val == "None": + val = None + else: + attr = getattr(config, key) + # coerce val type to attr's type unless attr is set to None (then leave as-is ) + try: + if attr is not None: + val = type(attr)(val) + except ValueError as x: + # allow setting to None + logger.warning( + "cannot set option (type mismatch) %s=%s --> %s", + key, + repr(val), + x, + ) + continue + except AttributeError: + # if attr unknown then leave the val as-is (string) + pass + + setattr(config, key, val) + + log_config(level=logging.DEBUG) - return logging.getLogger('.'.join(['smash',name])) + return config From 48fa6f59108681276e367cb5d3d8e59e94aa42e9 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:33:03 +0530 Subject: [PATCH 4/5] Revert "Complete basic loguru setup" This reverts commit 21ec9ea385efc3173323944ca37ecbe486d4c655. --- poetry.lock | 30 +----- pyproject.toml | 1 - python/smashbox/config.py | 29 +++-- python/smashbox/script.py | 220 ++++++++++++++++++++------------------ 4 files changed, 135 insertions(+), 145 deletions(-) diff --git a/poetry.lock b/poetry.lock index 1661db49..68d4dc2a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -147,21 +147,6 @@ category = "main" optional = false python-versions = "*" -[[package]] -name = "loguru" -version = "0.6.0" -description = "Python logging made (stupidly) simple" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["sphinx-rtd-theme (>=0.4.3)", "sphinx-autobuild (>=0.7.1)", "Sphinx (>=4.1.1)", "isort (>=5.1.1)", "black (>=19.10b0)", "pytest-cov (>=2.7.1)", "pytest (>=4.6.2)", "tox (>=3.9.0)", "flake8 (>=3.7.7)", "docutils (==0.16)", "colorama (>=0.3.4)"] - [[package]] name = "mypy-extensions" version = "0.4.3" @@ -314,21 +299,10 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "win32-setctime" -version = "1.1.0" -description = "A small Python utility to set file creation time on Windows" -category = "main" -optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "ed95f858055b4ddd0367fee27b6a6f4567edbace8ed1d73d09c2c3193c26019d" +content-hash = "e3392be6cf4d86dd2184ea6b7ab83d6e63ebdf93c4c708c0c45939268ef8ca96" [metadata.files] anyio = [ @@ -400,7 +374,6 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] -loguru = [] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -448,4 +421,3 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] typing-extensions = [] -win32-setctime = [] diff --git a/pyproject.toml b/pyproject.toml index a4994bee..a9a62413 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,6 @@ pytest = "^7.1.2" httpx = "^0.23.0" pydantic = "^1.9.1" PyYAML = "^6.0" -loguru = "^0.6.0" [tool.poetry.dev-dependencies] black = "^22.3.0" diff --git a/python/smashbox/config.py b/python/smashbox/config.py index d0d821a6..cfcb27c3 100644 --- a/python/smashbox/config.py +++ b/python/smashbox/config.py @@ -1,15 +1,25 @@ +import logging import os.path import pickle -from functools import cache from pathlib import Path from typing import Any, Literal import yaml -from loguru import logger from pydantic import BaseSettings -main_config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'etc', 'smashbox.conf.yaml') +logger = None + +# this should probably be moved into a utilities module +def get_logger(name: str = "config", level: int | None = None) -> logging.Logger: + global logger + if not logger: + if level is None: + level = ( + logging.INFO + ) # change here to DEBUG if you want to debug config stuff + logging.basicConfig(level=level) + return logging.getLogger(".".join(["smash", name])) class Configuration(BaseSettings): @@ -64,7 +74,8 @@ def _dict(self, **args: Any) -> dict[str, object]: def get(self, key: str, default: object) -> object: """Returns the value of the specified setting, or the default if the key doesn't exist.""" - logger.debug("config.get({} ,default={})", key, default) + logger = get_logger() + logger.debug("config.get(%s,default=%s)", key, default) return self._dict().get(key, default=default) @@ -74,15 +85,15 @@ def log_config( """Dump the entire configuration to the logging system at the given level. If hide_password=True then do not show the real value of the options which contain "password" in their names. """ + logger = get_logger() for key, val in config.dict().items(): if hide_password and "password" in key: val = "***" - logger.log(level, "CONFIG: {} = {}", key, val) + logger.log(level, "CONFIG: %s = %s", key, val) + -@cache() -def load_config(fp: Path | str = main_config_file) -> Configuration: - """Loads and parses the specified configuration file. - This function call is cached, that is it will return the same object for a given file path.""" +def load_config(fp: Path | str) -> Configuration: + """Loads and parses the specified configuration file.""" with open(fp, "r") as file: return Configuration(**yaml.load(file)) diff --git a/python/smashbox/script.py b/python/smashbox/script.py index 4bdc3355..0a783fea 100644 --- a/python/smashbox/script.py +++ b/python/smashbox/script.py @@ -1,121 +1,129 @@ -import logging -import os.path -from pathlib import Path -from typing import List, Sequence, Tuple - -from loguru import logger - import smashbox.compatibility.argparse as argparse -from smashbox.config import load_config, log_config +def keyval_tuple(x): + a,b = x.split('=',1) + return (a.strip(),b) -main_config_file = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - "etc", - "smashbox.conf.yaml", -) +def arg_parser(**kwds): + """ Create an ArgumentParser with common options for smash scripts and tools. + """ + + parser = argparse.ArgumentParser(**kwds) + + parser.add_argument('--option', '-o', metavar="key=val", dest="options", type=keyval_tuple, action='append', help='set config option') + parser.add_argument('--dry-run', '-n', action='store_true', help='show config options and print what tests would be run') + parser.add_argument('--quiet', '-q', action="store_true", help='do not produce output (other than errors)') + parser.add_argument('--verbose', '-v', action="store_true", help='produce more output') + parser.add_argument('--debug', action="store_true", help='produce very verbose output') + parser.add_argument('--config','-c',dest="configs",default=[],action="append",help='config files (one or more), added on to of default config file') + return parser -def keyval_tuple(x: str) -> Tuple[str, str]: - a, b = x.split("=", 1) - return (a.strip(), b) +import os.path +main_config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),'etc','smashbox.conf') +class Configuration: + # you may use config object for string interpolation "..."%config + def __getitem__(self,x): + return getattr(self,x) -def arg_parser(**kwds): - """Create an ArgumentParser with common options for smash scripts and tools.""" + def _dict(self,**args): + return dict(self.__dict__.items() + args.items()) - parser = argparse.ArgumentParser(**kwds) + def get(self,x,default): + logger = getLogger() + logger.debug('config.get(%s,default=%s)',repr(x),repr(default)) + try: + return getattr(self,x) + except AttributeError: + return default - parser.add_argument( - "--option", - "-o", - metavar="key=val", - dest="options", - type=keyval_tuple, - action="append", - help="set config option", - ) - parser.add_argument( - "--dry-run", - "-n", - action="store_true", - help="show config options and print what tests would be run", - ) - parser.add_argument( - "--quiet", - "-q", - action="store_true", - help="do not produce output (other than errors)", - ) - parser.add_argument( - "--verbose", "-v", action="store_true", help="produce more output" - ) - parser.add_argument( - "--debug", action="store_true", help="produce very verbose output" - ) - parser.add_argument( - "--config", - "-c", - dest="configs", - default=[], - action="append", - help="config files (one or more), added on to of default config file", - ) - return parser +config = Configuration() + +def configure_from_blob(config_blob): + import pickle + global config + config = pickle.loads(config_blob) + config_log(level=logging.DEBUG) + return config -def configure( - cmdline_opts: Sequence[Tuple[str, str]], - config_files: List[Path | str] | None = None, -): - """Initialize config object and return it. +def dump_config_to_blob(): + import pickle + return pickle.dumps(config) + +def configure(cmdline_opts,config_files=None): + """ Initialize config object and return it. + + First exec the sequence of config_files (including the + main_config_file). All symbols defined by these files will be set + as attributes of the config object. + + Then process cmdline_opts (which is a list of tuples generated by + arg_parser). If attribute matching the option already exists (was + defined in a configuration file) then eval to the same type (if not + None). Otherwise leave string values. The string "None" is special + and it is always converted to None and may always be assigned. + + """ + + if config_files is None: + config_files = [] + + logger = getLogger() + + config_files = [main_config_file] + config_files + + for cf in config_files: + execfile(cf,{},config.__dict__) + + if cmdline_opts: + for key,val in cmdline_opts: + try: + if val == "None": + val = None + else: + attr = getattr(config,key) + # coerce val type to attr's type unless attr is set to None (then leave as-is ) + try: + if attr is not None: + val = type(attr)(val) + except ValueError,x: + # allow setting to None + logger.warning("cannot set option (type mismatch) %s=%s --> %s",key,repr(val),x) + continue + except AttributeError: + # if attr unknown then leave the val as-is (string) + pass + + setattr(config,key,val) + + config_log(level=logging.DEBUG) + + return config + +def config_log(level,hide_password=False): + """ Dump the entire configuration to the logging system at the given level. + If hide_password=True then do not show the real value of the options which contain "password" in their names. + """ + logger = getLogger() + for d in dir(config): + if not d.startswith("_") and d != "get": + value = repr(getattr(config,d)) + if hide_password and 'password' in d: + value = "***" + + logger.log(level,"CONFIG: %s = %s",d,value) - First exec the sequence of config_files (including the - main_config_file). All symbols defined by these files will be set - as attributes of the config object. - Then process cmdline_opts (which is a list of tuples generated by - arg_parser). If attribute matching the option already exists (was - defined in a configuration file) then eval to the same type (if not - None). Otherwise leave string values. The string "None" is special - and it is always converted to None and may always be assigned. +import logging - """ - config = load_config() - if config_files is None: - config_files = [] - - config_files = [main_config_file] + config_files - - # for cf in config_files: - # execfile(cf, {}, config.__dict__) - - if cmdline_opts: - for key, val in cmdline_opts: - try: - if val == "None": - val = None - else: - attr = getattr(config, key) - # coerce val type to attr's type unless attr is set to None (then leave as-is ) - try: - if attr is not None: - val = type(attr)(val) - except ValueError as x: - # allow setting to None - logger.warning( - "cannot set option (type mismatch) %s=%s --> %s", - key, - repr(val), - x, - ) - continue - except AttributeError: - # if attr unknown then leave the val as-is (string) - pass - - setattr(config, key, val) - - log_config(level=logging.DEBUG) +logger = None +def getLogger(name="",level=None): + global logger + if not logger: + if level is None: + level = logging.INFO # change here to DEBUG if you want to debug config stuff + logging.basicConfig(level=level) - return config + return logging.getLogger('.'.join(['smash',name])) From 906c6f08fc0e9d599476c60368906b5165a8f166 Mon Sep 17 00:00:00 2001 From: Anand Krishna <40204976+anand2312@users.noreply.github.com> Date: Fri, 2 Sep 2022 22:57:10 +0530 Subject: [PATCH 5/5] Don't cache logger --- python/smashbox/config.py | 21 ++-- python/smashbox/script.py | 228 ++++++++++++++++++-------------------- python/smashbox/webdav.py | 2 +- 3 files changed, 118 insertions(+), 133 deletions(-) diff --git a/python/smashbox/config.py b/python/smashbox/config.py index cfcb27c3..35d192f3 100644 --- a/python/smashbox/config.py +++ b/python/smashbox/config.py @@ -8,17 +8,13 @@ from pydantic import BaseSettings -logger = None - # this should probably be moved into a utilities module def get_logger(name: str = "config", level: int | None = None) -> logging.Logger: - global logger - if not logger: - if level is None: - level = ( - logging.INFO - ) # change here to DEBUG if you want to debug config stuff - logging.basicConfig(level=level) + if level is None: + level = ( + logging.INFO + ) # change here to DEBUG if you want to debug config stuff + logging.basicConfig(level=level) return logging.getLogger(".".join(["smash", name])) @@ -80,7 +76,7 @@ def get(self, key: str, default: object) -> object: def log_config( - config: Configuration, level: int | None = None, hide_password: bool = False + config: Configuration, level: int = logging.DEBUG, hide_password: bool = False ) -> None: """Dump the entire configuration to the logging system at the given level. If hide_password=True then do not show the real value of the options which contain "password" in their names. @@ -95,11 +91,12 @@ def log_config( def load_config(fp: Path | str) -> Configuration: """Loads and parses the specified configuration file.""" with open(fp, "r") as file: - return Configuration(**yaml.load(file)) + return Configuration(**yaml.load(file, Loader=yaml.Loader)) def configure_from_blob(fp: Path | str) -> Configuration: - return pickle.load(fp) + with open(fp, "rb") as file: + return pickle.load(file) def dump_config(config: Configuration, fp: Path | str) -> None: diff --git a/python/smashbox/script.py b/python/smashbox/script.py index 0a783fea..80e7719f 100644 --- a/python/smashbox/script.py +++ b/python/smashbox/script.py @@ -1,129 +1,117 @@ +import logging +import os.path +from pathlib import Path +from typing import List, Sequence, Tuple + import smashbox.compatibility.argparse as argparse +from smashbox.config import load_config, log_config, get_logger, Configuration -def keyval_tuple(x): - a,b = x.split('=',1) - return (a.strip(),b) -def arg_parser(**kwds): - """ Create an ArgumentParser with common options for smash scripts and tools. - """ - - parser = argparse.ArgumentParser(**kwds) - - parser.add_argument('--option', '-o', metavar="key=val", dest="options", type=keyval_tuple, action='append', help='set config option') - parser.add_argument('--dry-run', '-n', action='store_true', help='show config options and print what tests would be run') - parser.add_argument('--quiet', '-q', action="store_true", help='do not produce output (other than errors)') - parser.add_argument('--verbose', '-v', action="store_true", help='produce more output') - parser.add_argument('--debug', action="store_true", help='produce very verbose output') - parser.add_argument('--config','-c',dest="configs",default=[],action="append",help='config files (one or more), added on to of default config file') - return parser +main_config_file = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "etc", + "smashbox.conf.yaml", +) -import os.path -main_config_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))),'etc','smashbox.conf') - -class Configuration: - # you may use config object for string interpolation "..."%config - def __getitem__(self,x): - return getattr(self,x) - - def _dict(self,**args): - return dict(self.__dict__.items() + args.items()) - - def get(self,x,default): - logger = getLogger() - logger.debug('config.get(%s,default=%s)',repr(x),repr(default)) - try: - return getattr(self,x) - except AttributeError: - return default - - -config = Configuration() - -def configure_from_blob(config_blob): - import pickle - global config - config = pickle.loads(config_blob) - config_log(level=logging.DEBUG) - return config - -def dump_config_to_blob(): - import pickle - return pickle.dumps(config) - -def configure(cmdline_opts,config_files=None): - """ Initialize config object and return it. - - First exec the sequence of config_files (including the - main_config_file). All symbols defined by these files will be set - as attributes of the config object. - - Then process cmdline_opts (which is a list of tuples generated by - arg_parser). If attribute matching the option already exists (was - defined in a configuration file) then eval to the same type (if not - None). Otherwise leave string values. The string "None" is special - and it is always converted to None and may always be assigned. - - """ - - if config_files is None: - config_files = [] - - logger = getLogger() - - config_files = [main_config_file] + config_files - - for cf in config_files: - execfile(cf,{},config.__dict__) - - if cmdline_opts: - for key,val in cmdline_opts: - try: - if val == "None": - val = None - else: - attr = getattr(config,key) - # coerce val type to attr's type unless attr is set to None (then leave as-is ) - try: - if attr is not None: - val = type(attr)(val) - except ValueError,x: - # allow setting to None - logger.warning("cannot set option (type mismatch) %s=%s --> %s",key,repr(val),x) - continue - except AttributeError: - # if attr unknown then leave the val as-is (string) - pass - - setattr(config,key,val) - - config_log(level=logging.DEBUG) - - return config - -def config_log(level,hide_password=False): - """ Dump the entire configuration to the logging system at the given level. - If hide_password=True then do not show the real value of the options which contain "password" in their names. - """ - logger = getLogger() - for d in dir(config): - if not d.startswith("_") and d != "get": - value = repr(getattr(config,d)) - if hide_password and 'password' in d: - value = "***" - - logger.log(level,"CONFIG: %s = %s",d,value) +def keyval_tuple(x: str) -> Tuple[str, str]: + a, b = x.split("=", 1) + return (a.strip(), b) -import logging +def arg_parser(**kwds): + """Create an ArgumentParser with common options for smash scripts and tools.""" -logger = None -def getLogger(name="",level=None): - global logger - if not logger: - if level is None: - level = logging.INFO # change here to DEBUG if you want to debug config stuff - logging.basicConfig(level=level) + parser = argparse.ArgumentParser(**kwds) - return logging.getLogger('.'.join(['smash',name])) + parser.add_argument( + "--option", + "-o", + metavar="key=val", + dest="options", + type=keyval_tuple, + action="append", + help="set config option", + ) + parser.add_argument( + "--dry-run", + "-n", + action="store_true", + help="show config options and print what tests would be run", + ) + parser.add_argument( + "--quiet", + "-q", + action="store_true", + help="do not produce output (other than errors)", + ) + parser.add_argument( + "--verbose", "-v", action="store_true", help="produce more output" + ) + parser.add_argument( + "--debug", action="store_true", help="produce very verbose output" + ) + parser.add_argument( + "--config", + "-c", + dest="configs", + default=[], + action="append", + help="config files (one or more), added on to of default config file", + ) + return parser + + +def configure( + cmdline_opts: Sequence[Tuple[str, str]], + config_files: List[Path | str] | None = None, +) -> Configuration: + """Initialize config object and return it. + First read the sequence of config_files (including the + main_config_file). All symbols defined by these files will be set + as attributes of the config object. + Then process cmdline_opts (which is a list of tuples generated by + arg_parser). If attribute matching the option already exists (was + defined in a configuration file) then eval to the same type (if not + None). Otherwise leave string values. The string "None" is special + and it is always converted to None and may always be assigned. + """ + logger = get_logger() + config = load_config(main_config_file) + if config_files is None: + config_files = [] + + config_files = [main_config_file] + config_files + + # for cf in config_files: + # execfile(cf, {}, config.__dict__) + + if cmdline_opts: + for key, val in cmdline_opts: + try: + if val == "None": + val = None + else: + attr = getattr(config, key) + # coerce val type to attr's type unless attr is set to None (then leave as-is ) + try: + if attr is not None: + val = type(attr)(val) + except ValueError as x: + # allow setting to None + logger.warning( + "cannot set option (type mismatch) %s=%s --> %s", + key, + repr(val), + x, + ) + continue + except AttributeError: + # if attr unknown then leave the val as-is (string) + pass + + setattr(config, key, val) + + log_config(config, level=logging.DEBUG) + + return config \ No newline at end of file diff --git a/python/smashbox/webdav.py b/python/smashbox/webdav.py index 2a6b5694..cef9c53c 100644 --- a/python/smashbox/webdav.py +++ b/python/smashbox/webdav.py @@ -8,7 +8,7 @@ logger = get_logger() -RequestType = Literal["MOVE", "PROPFIND", "PUT", "GET", "MKCOL", "DELTETE"] +RequestType = Literal["MOVE", "PROPFIND", "PUT", "GET", "MKCOL", "DELETE"] PropfindDepth = Literal["1", "0", "infinity"] OverwriteType = Literal["T", "F"]