From 53f06dfcc823384fa53e65fbd6a328468f78ac1b Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Sun, 13 Aug 2023 21:00:09 -0300 Subject: [PATCH 1/9] internal improvements --- .tool-versions | 1 + Makefile | 4 ++++ config/__init__.py | 2 +- config/cli.py | 43 +++++++++++++++++++++++---------------- requirements-dev.txt | 2 ++ setup.cfg | 6 +++--- tests/unit/test_cf.py | 7 +++++++ tests/unit/test_cfenv.py | 16 +++++++++++++++ tests/unit/test_cli.py | 14 +++++++++---- tests/unit/test_oauth2.py | 15 ++++++++++++++ tests/unit/test_spring.py | 8 ++++++++ 11 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..2ab8199 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.11.2 diff --git a/Makefile b/Makefile index 0dce407..1e141e1 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ ifeq ($(SKIP_STYLE), ) black config black tests endif + @echo "> running bandit" + bandit -r -ll -ii -s B104 config + @echo "> running radon" + radon cc -s -n C config tests @echo "> running flake8..." flake8 config flake8 tests diff --git a/config/__init__.py b/config/__init__.py index c202849..4490e9e 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -3,7 +3,7 @@ from .cfenv import CFenv from .spring import ConfigClient, config_client, create_config_client -__version__ = "1.3.0" +__version__ = "1.4.0" __all__ = [ "__version__", "ConfigClient", diff --git a/config/cli.py b/config/cli.py index 6934a99..d17c172 100644 --- a/config/cli.py +++ b/config/cli.py @@ -82,16 +82,7 @@ def client( ) if file: - # get file from server and exit - with Status("Contacting server...", spinner="dots4") as status: - try: - resp = client.get_file(file) - except RequestFailedException: - raise click.ClickException("💥 Failed to contact server!") - Path(file).write_text(resp) - status.update("OK!") - console.print(f"File saved: [cyan]{file}[/cyan]", style="bold") - raise SystemExit + _download_file(file, client) if verbose: table = Table.grid(padding=(0, 1)) @@ -114,14 +105,7 @@ def client( with Status("Contacting server...", spinner="dots4") as status: emoji = random.choice(EMOJI_ERRORS) try: - if auth: - username, password = auth.split(":") - auth = HTTPBasicAuth(username, password) - elif digest: - username, password = digest.split(":") - auth = HTTPDigestAuth(username, password) - else: - auth = None + auth = _configure_auth(auth, digest) client.get_config(auth=auth) except ValueError: raise click.ClickException( @@ -160,6 +144,29 @@ def client( ) +def _download_file(file, client): + """Get file from server and exit.""" + with Status("Contacting server...", spinner="dots4") as status: + try: + resp = client.get_file(file) + except RequestFailedException: + raise click.ClickException("💥 Failed to contact server!") + Path(file).write_text(resp) + status.update("OK!") + console.print(f"File saved: [cyan]{file}[/cyan]", style="bold") + raise SystemExit + + +def _configure_auth(basic_auth, digest_auth): + if basic_auth: + username, password = basic_auth.split(":") + return HTTPBasicAuth(username, password) + elif digest_auth: + username, password = digest_auth.split(":") + return HTTPDigestAuth(username, password) + return None + + @cli.command() @click.argument("text") @click.option( diff --git a/requirements-dev.txt b/requirements-dev.txt index 9ef2c14..d42b1ad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,8 @@ build types-requests requests-mock pre-commit +radon +bandit # flask integration Flask # aiohttp integration diff --git a/setup.cfg b/setup.cfg index e7c5d4c..8d45d89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,13 +4,13 @@ universal = 1 [metadata] name = config-client version = attr: config.__version__ -author = alexandre menezes +author = Alexandre Menezes author_email = alexandre.fmenezes@gmail.com description = config client for Spring Cloud Config Server and Cloud Foundry long_description = file: README.md long_description_content_type = text/markdown license = Apache-2.0 -license_file = LICENSE +license_files = LICENSE url = https://github.com/amenezes/config-client project_urls = Documentation = https://config-client.amenezes.net @@ -84,7 +84,7 @@ warn_unused_ignores = True warn_unreachable = True [tox:tox] -envlist = py{37,38,39,310,311},pypy{3.6,3.7,3.8} +envlist = py{37,38,39,310,311},pypy{3.6,3.7,3.8,3.9,3.10} [testenv] deps = -rrequirements-dev.txt diff --git a/tests/unit/test_cf.py b/tests/unit/test_cf.py index d111035..19156a4 100644 --- a/tests/unit/test_cf.py +++ b/tests/unit/test_cf.py @@ -30,6 +30,13 @@ def test_configure_custom_client(): assert cf.client.address == "http://localhost:8888/configuration" +@pytest.mark.parametrize( + "attr", ["cfenv", "oauth2", "client", "vcap_services", "vcap_application", "config"] +) +def test_cf_attributes(cf, attr): + assert hasattr(cf, attr) + + def test_default_properties(cf): assert cf is not None diff --git a/tests/unit/test_cfenv.py b/tests/unit/test_cfenv.py index a4cff76..20da999 100644 --- a/tests/unit/test_cfenv.py +++ b/tests/unit/test_cfenv.py @@ -64,3 +64,19 @@ def test_custom_vcap_configserver_client_id(custom_cfenv): def test_custom_vcap_configserver_client_secret(custom_cfenv): assert custom_cfenv.configserver_client_secret() == "example_client_secret" + + +@pytest.mark.parametrize( + "attr", + [ + "vcap_service_prefix", + "vcap_application", + "vcap_services", + "space_name", + "organization_name", + "application_name", + "uris", + ], +) +def test_cfenv_attributes(cfenv, attr): + assert hasattr(cfenv, attr) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index eec29b5..ee7db88 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -101,14 +101,20 @@ class TestDecryptCommand: def _mock_decrypt(self, *args, **kwargs): return "123" - def test_decrypt_command(self, cli_runner, monkeypatch): + @pytest.mark.parametrize( + "secret", + [ + "dfa76862fe7f367d9c1923de55ba85512eea7a41163ade3059d64fcfbed31017", + "{cipher}dfa76862fe7f367d9c1923de55ba85512eea7a41163ade3059d64fcfbed31017", + ], + ) + def test_decrypt_command(self, cli_runner, monkeypatch, secret): monkeypatch.setattr(ConfigClient, "decrypt", self._mock_decrypt) result = cli_runner.invoke( decrypt, - ["dfa76862fe7f367d9c1923de55ba85512eea7a41163ade3059d64fcfbed31017"], + [secret], ) - assert result.output is not None - assert len(result.output) >= 100 + assert "123" in result.output def test_decrypt_command_error(self, cli_runner, monkeypatch): monkeypatch.setattr(http, "post", SystemExit()) diff --git a/tests/unit/test_oauth2.py b/tests/unit/test_oauth2.py index 2ca282d..cc59b31 100644 --- a/tests/unit/test_oauth2.py +++ b/tests/unit/test_oauth2.py @@ -31,3 +31,18 @@ def test_authorization_header(oauth2, monkeypatch): monkeypatch.setattr(http, "post", conftest.oauth2_mock) oauth2.configure() assert isinstance(oauth2.authorization_header, dict) + + +@pytest.mark.parametrize( + "attr", + [ + "access_token_uri", + "client_id", + "client_secret", + "grant_type", + "token", + "authorization_header", + ], +) +def test_oauth_attributes(oauth2, attr): + assert hasattr(oauth2, attr) diff --git a/tests/unit/test_spring.py b/tests/unit/test_spring.py index 1b251ee..7a7254a 100644 --- a/tests/unit/test_spring.py +++ b/tests/unit/test_spring.py @@ -211,3 +211,11 @@ def test_client_with_auth_and_headers(monkeypatch, mocker, oauth2): }, verify=False, ) + + +@pytest.mark.parametrize( + "attr", + ["address", "label", "app_name", "profile", "fail_fast", "oauth2", "url", "config"], +) +def test_config_client_attributes(client, attr): + assert hasattr(client, attr) From 43088bb0ad7bf9bf387f2a31200a6cca3dd78b0d Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Mon, 14 Aug 2023 13:17:47 -0300 Subject: [PATCH 2/9] updated project documentation --- config/auth.py | 6 +- config/cf.py | 41 +++++++++++++- config/exceptions.py | 8 ++- config/ext/aiohttp.py | 18 ++++++ config/ext/fastapi.py | 34 +++++++++++ config/ext/flask.py | 16 ++++++ config/spring.py | 127 ++++++++++++++++++++++++++++++++++++------ 7 files changed, 227 insertions(+), 23 deletions(-) diff --git a/config/auth.py b/config/auth.py index 3d75556..9cb5642 100644 --- a/config/auth.py +++ b/config/auth.py @@ -37,11 +37,11 @@ def request_token(self, client_auth: HTTPBasicAuth, data: dict, **kwargs) -> Non self.access_token_uri, auth=client_auth, data=data, **kwargs ) except MissingSchema: - raise RequestFailedException("Access token URL it's empty") + raise RequestFailedException("empty") except HTTPError: - raise RequestTokenException("Failed to retrieve oauth2 access_token.") + raise RequestTokenException self.token = response.json().get("access_token") - logger.info("Access token successfully obtained.") + logger.debug("Access token successfully obtained.") def configure(self, **kwargs) -> None: client_auth = HTTPBasicAuth(self.client_id, self.client_secret) diff --git a/config/cf.py b/config/cf.py index ab06e12..c73ae7b 100644 --- a/config/cf.py +++ b/config/cf.py @@ -41,16 +41,55 @@ def vcap_application(self): return self.cfenv.vcap_application def get_config(self, **kwargs) -> None: + """Request the configuration to the config server. + + Usage: + + # Example 1: + cf.get_config() + + # Example 2: + cf.get_config(verify=False) + + :param kwargs: any keyword argument used to configure oauth2 or request for the server. + """ self.client.get_config(**kwargs) async def get_config_async(self, **kwargs) -> None: + """Request the configuration to the config server. + + Usage: + + # Example 1: + await cf.get_config_async() + + # Example 2: + await cf.get_config_async(verify=False) + + :param kwargs: any keyword argument used to configure oauth2 or request for the server. + """ await self.client.get_config_async(**kwargs) @property def config(self) -> Dict: + """Getter from configurations retrieved from ConfigClient.""" return self.client.config - def get(self, key, default: Any = ""): + def get(self, key: str, default: Any = "") -> Any: + """Loads a configuration from a key. + + Usage: + + # Example 1: + cf.get('spring') + + # Exampel 2: + cf.get('spring.cloud.consul') + + + :param key: configuration key. + :param default: default value if key does not exist. [default='']. + """ return self.client.get(key, default) def keys(self) -> KeysView: diff --git a/config/exceptions.py b/config/exceptions.py index 1688e72..284a68c 100644 --- a/config/exceptions.py +++ b/config/exceptions.py @@ -1,6 +1,10 @@ class RequestTokenException(Exception): - pass + def __init__(self, message: str = "Faield to retrieve oauth2 access_token") -> None: + super().__init__(message) class RequestFailedException(Exception): - pass + def __init__( + self, url: str, message: str = "Failed to perform request: [URL='{url}']" + ) -> None: + super().__init__(message.format(url=url)) diff --git a/config/ext/aiohttp.py b/config/ext/aiohttp.py index 23cccf6..5a032d9 100644 --- a/config/ext/aiohttp.py +++ b/config/ext/aiohttp.py @@ -15,6 +15,24 @@ def __init__( client: Optional[ConfigClient] = None, **kwargs, ) -> None: + """Configure AIOHTTP application with config-client. + + Usage: + + from config.ext import AioHttpConfig + from aiohttp import web + + + app = web.Application() + AioHttpConfig(app) + + web.run_app(app) + + + :param app: AIOHTTP web.Application. + :param key: key prefix to access config. + :param client: custom ConfigClient. + """ self._validate_app(app) if not client: client = ConfigClient() diff --git a/config/ext/fastapi.py b/config/ext/fastapi.py index bf4fd22..8c6d291 100644 --- a/config/ext/fastapi.py +++ b/config/ext/fastapi.py @@ -5,6 +5,23 @@ async def fastapi_config_client(request: Request): + """Configure FastAPI application with config-client. + + Usage: + + from fastapi import Depends, FastAPI + from config.ext.fastapi import fastapi_config_client + + + app = FastAPI(dependencies=[Depends(fastapi_config_client)]) + + @app.get("/info") + def consul(request: Request): + return dict( + description=request.app.config_client.get("info.app.description"), + url=request.app.config_client.get("info.app.name"), + ) + """ try: request.app.config_client logger.debug("ConfigClient already initialized") @@ -17,6 +34,23 @@ async def fastapi_config_client(request: Request): async def fastapi_cloud_foundry(request: Request): + """Configure FastAPI application with config-client. + + Usage: + + from fastapi import Depends, FastAPI + from config.ext.fastapi import fastapi_cloud_foundry + + + app = FastAPI(dependencies=[Depends(fastapi_cloud_foundry)]) + + @app.get("/info") + def consul(request: Request): + return dict( + description=request.app.config_client.get("info.app.description"), + url=request.app.config_client.get("info.app.name"), + ) + """ try: logger.debug("ConfigClient already initialized") except AttributeError: diff --git a/config/ext/flask.py b/config/ext/flask.py index fc149fd..0621d39 100644 --- a/config/ext/flask.py +++ b/config/ext/flask.py @@ -20,6 +20,22 @@ class FlaskConfig: def __init__( self, app: Flask, client: Optional[ConfigClient] = None, **kwargs ) -> None: + """Configure Flask application with config-client. + + Usage: + + from config.ext.flask import FlaskConfig + from flask import Flask + + + app = Flask(__name__) + FlaskConfig(app) + + + :param app: Flask application. + :param client: custom ConfigClient. + :param kwargs: any keyword argument used to request config from the server. + """ if not isinstance(app, Flask): raise TypeError("app must be Flask instance") diff --git a/config/spring.py b/config/spring.py index cdd0f72..ea721d6 100644 --- a/config/spring.py +++ b/config/spring.py @@ -54,10 +54,22 @@ class ConfigClient: @property def url(self) -> str: + """URL that will be used to request config.""" return f"{self.address}/{self.app_name}/{self.profile}/{self.label}" def get_config(self, **kwargs) -> None: - """Request the configuration from the config server.""" + """Request the configuration to the config server. + + Usage: + + # Example 1: + client.get_config() + + # Example 2: + client.get_config(verify=False) + + :param kwargs: any keyword argument used to configure oauth2 or request for the server. + """ kwargs = self._configure_oauth2(**kwargs) try: response = http.get(self.url, **kwargs) @@ -66,7 +78,7 @@ def get_config(self, **kwargs) -> None: logger.error(err) if self.fail_fast: logger.info("fail_fast enabled. Terminating process.") - raise SystemExit(1) + raise SystemExit("fail_fast enabled. Terminating process.") raise ConnectionError("fail_fast disabled.") fconfig = [ to_dict(config) @@ -79,6 +91,18 @@ def get_config(self, **kwargs) -> None: merge_dict(self._config, server_config) async def get_config_async(self, **kwargs) -> None: + """Request the configuration to the config server. + + Usage: + + # Example 1: + await client.get_config_async() + + # Example 2: + await client.get_config_async(verify=False) + + :param kwargs: any keyword argument used to configure oauth2 or request for the server. + """ loop = asyncio.get_running_loop() await loop.run_in_executor(None, partial(self.get_config, **kwargs)) @@ -92,12 +116,21 @@ def _configure_oauth2(self, **kwargs) -> dict: return kwargs def get_file(self, filename: str, **kwargs: dict) -> str: - """Request a file from the config server.""" + """Request a file from the config server. + + Usage: + + clieng.get_file('nginx.conf') + + + :param filename: filename to retrieve from the server. + :param kwargs: any keyword argument used to configure request for the server. + """ uri = f"{self.address}/{self.app_name}/{self.profile}/{self.label}/{filename}" try: response = http.get(uri, **kwargs) except Exception: - raise RequestFailedException(f"Failed to request URI: {uri}") + raise RequestFailedException(f"{uri}") return response.text def encrypt( @@ -107,13 +140,24 @@ def encrypt( headers: dict = {"Content-Type": "text/plain"}, **kwargs: dict, ) -> str: - """Request a encryption from a value to the config server.""" + """Request a encryption of a value to the config server. + + Usage: + + client.encrypt('123') + + + :param value: value to encrypt. + :param path: base URL to encrypt. [default=/encrypt]. + :param headers: HTTP Headers to send to server. + :param kwargs: any keyword argument used to configure request for the server. + """ try: response = http.post( uri=f"{self.address}{path}", data=value, headers=headers, **kwargs ) except Exception: - raise RequestFailedException(f"Failed to request URI: {self.address}{path}") + raise RequestFailedException(f"{self.address}{path}") return response.text def decrypt( @@ -123,13 +167,24 @@ def decrypt( headers: dict = {"Content-Type": "text/plain"}, **kwargs: dict, ) -> str: - """Request a decryption from a value to the config server..""" + """Request decryption from a value to the config server. + + Usage: + + client.decrypt('35a51fc974e5df6779265239624c4b404ababf08093d1ca265b19bed4863f038') + + + :param value: value to decrypt. + :param path: base URL to decrypt. [default=/decrypt]. + :param headers: HTTP Headers to send to server. + :param kwargs: any keyword argument used to configure request for the server. + """ try: response = http.post( uri=f"{self.address}{path}", data=value, headers=headers, **kwargs ) except Exception: - raise RequestFailedException(f"Failed to request URI: {self.address}{path}") + raise RequestFailedException(f"{self.address}{path}") return response.text @property @@ -138,6 +193,20 @@ def config(self) -> Dict: return self._config def get(self, key: str, default: Any = "") -> Any: + """Loads a configuration from a key. + + Usage: + + # Example 1: + client.get('spring') + + # Exampel 2: + client.get('spring.cloud.consul') + + + :param key: configuration key. + :param default: default value if key does not exist. [default='']. + """ return glom(self._config, key, default=default) def keys(self) -> KeysView: @@ -149,15 +218,27 @@ def create_config_client(**kwargs) -> ConfigClient: """ Create ConfigClient singleton instance. - :param address: - :param app_name: - :param branch: - :param fail_fast: - :param profile: - :param url: + Usage: + + # Example 1: + client = create_config_client(app_name='simpleweb000') + + # Example 2: + client = create_config_client( + app_name='simpleweb000', + address='http://localhost:8888/configuration' + ) + + :param address: Spring Cloud Config Server. + :param label: branch used to retrieve configuration. + :param app_name: application name. + :param profile: config profile [default=development] + :param fail_fast: enable fail_fast [default=True]. + :param oauth2: Spring Cloud Config Server. + :return: ConfigClient instance. """ - instance_params, get_config_params = __get_params(**kwargs) + instance_params, get_config_params = _get_params(**kwargs) obj = ConfigClient(**instance_params) obj.get_config(**get_config_params) return obj @@ -168,14 +249,26 @@ def config_client(**kwargs) -> Callable[[Dict[str, str]], ConfigClient]: Usage: + # Example 1: @config_client(app_name='test') def get_config(config): db_user = config.get_attribute('database.user') + + # Example 2: + @config_client( + app_name='test', + address='http://localhost:8888/configuration' + ) + def get_config(config): + db_user = config.get_attribute('database.user') + + :raises: ConnectionError: If fail_fast enabled. + :return: ConfigClient instance. """ - instance_params, get_config_params = __get_params(**kwargs) + instance_params, get_config_params = _get_params(**kwargs) def wrap_function(function): logger.debug(f"caller: [name='{function.__name__}']") @@ -191,7 +284,7 @@ def enable_config(): return wrap_function -def __get_params(**kwargs) -> Tuple[Dict, Dict]: +def _get_params(**kwargs) -> Tuple[Dict, Dict]: instance_params = {} get_config_params = {} for key, value in kwargs.items(): From 11b279c331984c90db99632754751bf07e2326c9 Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Sat, 9 Sep 2023 20:21:40 -0300 Subject: [PATCH 3/9] added terminal-ui support via trogon --- config/cli.py | 2 ++ requirements-dev.txt | 2 ++ setup.cfg | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/config/cli.py b/config/cli.py index d17c172..cefe1e1 100644 --- a/config/cli.py +++ b/config/cli.py @@ -11,6 +11,7 @@ from rich.panel import Panel from rich.status import Status from rich.table import Table +from trogon import tui from config import __version__ from config.exceptions import RequestFailedException @@ -26,6 +27,7 @@ console = Console() +@tui(command="terminal-ui", help="Open terminal UI") @click.group(context_settings=CONTEXT_SETTINGS) @click.version_option(version=__version__) def cli(): diff --git a/requirements-dev.txt b/requirements-dev.txt index d42b1ad..8526f0e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -24,3 +24,5 @@ aiohttp fastapi # optional for asdf-vm tox-asdf +# terminal-ui +trogon diff --git a/setup.cfg b/setup.cfg index 8d45d89..dca35f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,9 +45,9 @@ install_requires = python_requires = >= 3.7 [options.extras_require] -cli = click>=8.1.3; rich>=12.6.0 +cli = click>=8.1.3; rich>=12.6.0; trogon>=0.5.0 docs = mkdocs-material -all = click>=8.1.3; mkdocs-material; rich>=12.6.0 +all = click>=8.1.3; mkdocs-material; rich>=12.6.0; trogon>=0.5.0 [options.entry_points] console_scripts = From 851f8ad0cc4eafb8606bd4c9d7fa86842b2b6325 Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Sat, 9 Sep 2023 20:44:21 -0300 Subject: [PATCH 4/9] fix textual version --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 8526f0e..27cc1bf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,3 +26,4 @@ fastapi tox-asdf # terminal-ui trogon +textual>=0.36.0 From 6df72a6ab56d99c136542c11b1cab9318c599c9f Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Thu, 28 Sep 2023 09:38:12 -0300 Subject: [PATCH 5/9] drop Python 3.7 from CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8fd679..c298020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: tests: strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] os: [ubuntu] fail-fast: true runs-on: ${{ matrix.os }}-latest From 3022ca3b1431d15d07462ba66b1b5b73fb4ca97b Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Mon, 23 Oct 2023 17:16:07 -0300 Subject: [PATCH 6/9] Added support to Python 3.12 --- .github/workflows/ci.yml | 2 +- config/_config.py | 2 +- config/auth.py | 4 +++- config/spring.py | 11 +++++------ setup.cfg | 3 ++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c298020..1412335 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: tests: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] os: [ubuntu] fail-fast: true runs-on: ${{ matrix.os }}-latest diff --git a/config/_config.py b/config/_config.py index ecc0200..872a00f 100644 --- a/config/_config.py +++ b/config/_config.py @@ -51,7 +51,7 @@ def merge_list(config: dict) -> None: config.pop(f"{key}[{i}]") -def _merge(config: dict): +def _merge(config: dict) -> None: merge_list(config) for k, v in config.items(): merge_list(config[k]) diff --git a/config/auth.py b/config/auth.py index 9cb5642..55de652 100644 --- a/config/auth.py +++ b/config/auth.py @@ -1,3 +1,5 @@ +from typing import Dict + from attrs import field, mutable, validators from requests.auth import HTTPBasicAuth from requests.exceptions import HTTPError, MissingSchema @@ -28,7 +30,7 @@ def token(self, value) -> None: logger.debug(f"set: [access_token='{self._token}']") @property - def authorization_header(self) -> dict: + def authorization_header(self) -> Dict[str, str]: return {"Authorization": f"Bearer {self.token}"} def request_token(self, client_auth: HTTPBasicAuth, data: dict, **kwargs) -> None: diff --git a/config/spring.py b/config/spring.py index ea721d6..904a4fd 100644 --- a/config/spring.py +++ b/config/spring.py @@ -1,11 +1,10 @@ """Module for retrieve application's config from Spring Cloud Config.""" import asyncio import os -from distutils.util import strtobool from functools import partial, wraps from typing import Any, Callable, Dict, KeysView, Optional, Tuple -from attrs import field, fields_dict, mutable, validators +from attrs import converters, field, fields_dict, mutable, validators from glom import glom from . import http @@ -36,10 +35,10 @@ class ConfigClient: default=os.getenv("PROFILE", "development"), validator=validators.instance_of(str), ) - fail_fast: bool = field( - default=bool(strtobool(str(os.getenv("CONFIG_FAIL_FAST", True)))), + fail_fast: bool = field( # type: ignore + default=os.getenv("CONFIG_FAIL_FAST", True), validator=validators.instance_of(bool), - converter=bool, + converter=converters.to_bool, ) oauth2: Optional[OAuth2] = field( default=None, @@ -188,7 +187,7 @@ def decrypt( return response.text @property - def config(self) -> Dict: + def config(self) -> dict: """Getter from configurations retrieved from ConfigClient.""" return self._config diff --git a/setup.cfg b/setup.cfg index dca35f5..eb9da0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries @@ -84,7 +85,7 @@ warn_unused_ignores = True warn_unreachable = True [tox:tox] -envlist = py{37,38,39,310,311},pypy{3.6,3.7,3.8,3.9,3.10} +envlist = py{37,38,39,310,311,312},pypy{3.8,3.9,3.10} [testenv] deps = -rrequirements-dev.txt From 301f28e89c903bde49e42199d69b8dbfd7598e92 Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Tue, 24 Oct 2023 11:16:29 -0300 Subject: [PATCH 7/9] Updated docs --- Makefile | 2 +- docs/client/asyncio.md | 39 +++++++++++++++++++++++-------- docs/client/cloudfoundry.md | 9 +++++--- docs/client/decorator.md | 12 +++------- docs/client/singleton.md | 6 ++--- docs/client/standard-client.md | 34 ++++++++++++++++----------- docs/command-line.md | 30 ++++++++++++------------ docs/development.md | 20 ++++++++-------- docs/integrations/aiohttp.md | 24 ++++++++++--------- docs/integrations/fastapi.md | 21 ++++++++--------- docs/integrations/flask.md | 26 ++++++++++----------- docs/overview.md | 42 ++++++++++++++++------------------ mkdocs.yml | 27 ++++++++++++++-------- requirements-dev.txt | 1 + 14 files changed, 162 insertions(+), 131 deletions(-) diff --git a/Makefile b/Makefile index 1e141e1..f671652 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ tests: docs: @echo "> generate project documentation..." @cp README.md docs/index.md - mkdocs serve + mkdocs serve -a 0.0.0.0:8000 install-deps: @echo "> installing dependencies..." diff --git a/docs/client/asyncio.md b/docs/client/asyncio.md index c2f7ae0..a872dce 100644 --- a/docs/client/asyncio.md +++ b/docs/client/asyncio.md @@ -1,8 +1,8 @@ -# Asyncio +## Native method -- native method `get_config_async` +- native method **`get_config_async`**: -```python +``` py linenums="1" from config import ConfigClient @@ -12,24 +12,43 @@ await cc.get_config_async(timeout=5.0) cc.config ``` -> For python 3.8 > use asyncio REPL. `python -m asyncio` +!!! tip "" -**NOTICE**: To avoid block the event loop in the calls made by `config-client` could you use: + For python 3.8 > use asyncio REPL. `python -m asyncio` -- for python > 3.9: [`asyncio.to_thread()`](https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread) as the example below: -````python +!!! warning "" + + If for some reason the get_config_async method cannot be used, there are still some other possibilities, such as: + + - [asyncio.to_thread](https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread) + - [run_in_executor](https://docs.python.org/3.8/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) + + This is important to avoid blocking the event loop. + +### asyncio.to_thread + +!!! tip "" + + For Python > 3.9 + +``` py linenums="1" from config.spring import ConfigClient cc = ConfigClient(app_name='foo', label='main') await asyncio.to_thread(cc.get_config) print(cc.config) -```` +``` + +### run_in_executor + +!!! tip "" + + For Python > 3.6 -- for python > 3.6: [`run_in_executor()`](https://docs.python.org/3.8/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) as the example below: -```python +``` py linenums="1" import asyncio import concurrent.futures diff --git a/docs/client/cloudfoundry.md b/docs/client/cloudfoundry.md index 3b98749..ba1004a 100644 --- a/docs/client/cloudfoundry.md +++ b/docs/client/cloudfoundry.md @@ -14,7 +14,7 @@ The value can be updated setting the environment variable `VCAP_SERVICE_PREFIX` Example of using the Cloud Foundry module: -````python +````py linenums="1" from config.cf import CF cf = CF() @@ -25,14 +25,17 @@ cf.get_config() Example of using the Cloud Foundry module: -```python +```py linenums="1" from config.cf import CF from config.cfenv import CFenv cf = CF(cfenv=CFenv(vcap_service_prefix="p.config-server")) cf.get_config() ``` -**notice**: if the environment variable `VCAP_SERVICE_PREFIX` was set to `p.config-server` cfenv parameter will not be necessary. +!!! tip "" + + if the environment variable `VCAP_SERVICE_PREFIX` was set to `p.config-server` cfenv parameter will not be necessary. + ## Notice diff --git a/docs/client/decorator.md b/docs/client/decorator.md index 257868a..6e0785e 100644 --- a/docs/client/decorator.md +++ b/docs/client/decorator.md @@ -2,15 +2,9 @@ ## Default values -For use cases where environment variables are set. +Assuming default values. -``` ini title=".env" -APP_NAME=foo -PROFILE=dev,docker -LABEL=main -``` - -```python +```py linenums="1" from config import config_client @@ -28,7 +22,7 @@ my_test() For use cases where environment variables are not set can you use decorator parameters, as example below: -```python +```py linenums="1" from config import config_client diff --git a/docs/client/singleton.md b/docs/client/singleton.md index fa64bdf..1330f95 100644 --- a/docs/client/singleton.md +++ b/docs/client/singleton.md @@ -2,7 +2,7 @@ ## Create singleton instance with default values -```python +``` py linenums="1" from config import create_config_client @@ -19,7 +19,7 @@ print(id(c2)) ## Create singleton instance with custom values -```python +``` py linenums="1" from config import create_config_client @@ -40,7 +40,7 @@ print(id(c2)) ## Singleton instance with decorator -```python +``` py linenums="1" from config import config_client from config.core import singleton diff --git a/docs/client/standard-client.md b/docs/client/standard-client.md index 50446af..f81a7f8 100644 --- a/docs/client/standard-client.md +++ b/docs/client/standard-client.md @@ -1,10 +1,8 @@ -# Standard Client - ## Usage ### Overview -``` py +``` py linenums="1" from config import ConfigClient @@ -23,7 +21,7 @@ cc.get('spring.cloud.config.uri') #### Custom parameters on HTTP request -```python +``` py linenums="1" from config import ConfigClient @@ -31,13 +29,21 @@ cc = ConfigClient(app_name='foo', label='main') cc.get_config(timeout=5.0, headers={'Accept': 'application/json'}) ``` -> hint: any parameter supported by the `get` method from the `requests` can be used on `get_config`. Including authentication, SSL verification or custom headers, for example. See: [#40](https://github.com/amenezes/config-client/issues/40) [#41](https://github.com/amenezes/config-client/issues/41) +!!! tip "" + + Any parameter supported by the **`get`** method from the [requests library](https://requests.readthedocs.io/en/latest/) can be used on **`get_config`**, including: authentication, SSL verification or custom headers. + + Details: + + - [How to invoke config server using basic authentication](https://github.com/amenezes/config-client/issues/40) + - [ Is there a option for https?](https://github.com/amenezes/config-client/issues/41) + ### Authentication #### OAuth2 -```python +``` py linenums="1" from config import ConfigClient from config.auth import OAuth2 @@ -55,7 +61,7 @@ cc.get_config() #### Basic -``` +``` py linenums="1" from requests.auth import HTTPBasicAuth from config import ConfigClient @@ -66,7 +72,7 @@ cc.get_config(auth=HTTPBasicAuth('user', 'passwd')) #### Digest -``` +``` py linenums="1" from requests.auth import HTTPDigestAuth from config import ConfigClient @@ -78,13 +84,15 @@ cc.get_config(auth=HTTPDigestAuth('user', 'passwd')) ### Retrieving plain files -````python +``` py linenums="1" from config import ConfigClient cc = ConfigClient(app_name='foo', label='main') -books = cc.get_file('books.xml') +cc.get_file('books.xml') +``` -print(books) -```` +!!! tip "" -> For more info see: [https://cloud.spring.io/spring-cloud-config/multi/multi__serving_plain_text.html](https://cloud.spring.io/spring-cloud-config/multi/multi__serving_plain_text.html) + For more details access: + + - [Serving Plain Text](https://cloud.spring.io/spring-cloud-config/multi/multi__serving_plain_text.html) diff --git a/docs/command-line.md b/docs/command-line.md index 7b64301..0f8abcc 100644 --- a/docs/command-line.md +++ b/docs/command-line.md @@ -1,6 +1,4 @@ -# CLI - -From the version >= `0.5.0` a simple command line it's available to query and test config-client. +From the version >= `0.5.0` a command line it's available to query and test config-client. ## Installing cli dependencies @@ -14,7 +12,7 @@ pip install 'config-client[cli]' python -m config ``` -``` bash title="expected output" +``` bash title="Example output" Usage: python -m config [OPTIONS] COMMAND [ARGS]... Options: @@ -33,7 +31,7 @@ Commands: python -m config client -h ``` -```bash title="expected output" +```bash title="Example output" Usage: python -m config client [OPTIONS] APP_NAME Interact with Spring Cloud Server via cli. @@ -52,9 +50,9 @@ Options: -h, --help Show this message and exit. ``` -> **`Notice`** +!!! tip "" -If you preferer can you set the command line options in `environment variables`. + If you preferer can you set the command line options in `environment variables`. Example of environment variables available to override the command line options. @@ -66,7 +64,7 @@ Command syntax: `config client ` python -m config client simpleweb000 -l master -v ``` -``` bash title="expected output" +``` bash title="Example output" ╭─────────────────────────────────────────────────────── client info ────────────────────────────────────────────────────────╮ │ address: http://localhost:8888 │ │ label: master │ @@ -182,7 +180,7 @@ Command syntax: `config client --json` python -m config client simpleweb000 -f spring.cloud.consul --json ``` -``` bash title="expected output" +``` bash title="Example output" File saved: response.json ``` @@ -194,7 +192,7 @@ Command syntax: `config client --file ` python -m config client simpleweb000 --file nginx.conf ``` -``` bash title="expected output" +``` bash title="Example output" File saved: nginx.conf ``` @@ -206,7 +204,7 @@ Command syntax: `config encrypt ` python -m config encrypt 123 ``` -```bash title="expected output" +```bash title="Example output" ╭─────────────────────────────────────────────────────────────────────────────────────╮ │ encrypted data: 'f6d620453e28359fa05a2a96f2a089f5a46d858ee0174f5506e73a526ac6aed2' │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ @@ -216,7 +214,7 @@ python -m config encrypt 123 python -m config encrypt 123 --raw ``` -```bash title="expected output" +```bash title="Example output" ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ encrypted data: '{cipher}59e4bf2fff4a0411eb216e617886f3464d1c0d5a13fec0c00b746ed007ef28d5' │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -231,7 +229,7 @@ Command syntax: `config decrypt ` python -m config decrypt {cipher}59e4bf2fff4a0411eb216e617886f3464d1c0d5a13fec0c00b746ed007ef28d5 ``` -``` bash title="expected output" +``` bash title="Example output" ╭────────────────────────╮ │ decrypted data: '123' │ ╰────────────────────────╯ @@ -241,7 +239,7 @@ python -m config decrypt {cipher}59e4bf2fff4a0411eb216e617886f3464d1c0d5a13fec0c python -m config decrypt 59e4bf2fff4a0411eb216e617886f3464d1c0d5a13fec0c00b746ed007ef28d5 ``` -``` bash title="expected output" +``` bash title="Example output" ╭────────────────────────╮ │ decrypted data: '123' │ ╰────────────────────────╯ @@ -255,7 +253,7 @@ Command syntax: `config decrypt --auth ` python -m config client simpleweb000 -f spring.cloud.consul --auth user:pass ``` -``` bash title="expected output" +``` bash title="Example output" ╭───────────────────────────────────────────── client info ─────────────────────────────────────────────╮ │ address: http://localhost:8888 │ │ label: master │ @@ -279,7 +277,7 @@ Command syntax: `config client --digest ` python -m config client simpleweb000 'spring.cloud.consul' --digest user:pass ``` -``` bash title="expected output" +``` bash title="Example output" ╭───────────────────────────────────────────── client info ─────────────────────────────────────────────╮ │ address: http://localhost:8888 │ │ label: master │ diff --git a/docs/development.md b/docs/development.md index beb4250..97dcb0a 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,23 +1,24 @@ -# Development - ## Install development dependencies -```bash +```bash linenums="1" make install-deps # OR: pip install -r requirements-dev.txt ``` ## Execute tests -```bash +```bash linenums="1" make tests -# OR: pytest +# OR: python -m pytest -vv --no-cov-on-fail --color=yes --cov-report term --cov=config tests ``` -## Generating documentation locally. +## Generating documentation locally -```bash +```bash linenums="1" pip install 'config-client[docs]' +``` + +```bash linenums="1" make docs ``` @@ -25,11 +26,10 @@ make docs If you would like to test spring-cloud-configserver locally can you use: -- the docker image `amenezes/spring-cloud-configserver` with [spring_config](https://github.com/amenezes/spring_config) examples; -- clone `config-client` and use `docker-compose up -d`; +- the docker image `amenezes/spring-cloud-configserver` with [spring_config](https://github.com/amenezes/spring_config) examples; OR - use [hyness/spring-cloud-config-server](https://github.com/hyness/spring-cloud-config-server) -```bash +```bash linenums="1" docker run -it --rm -p 8888:8888 \ hyness/spring-cloud-config-server:3.1.0-jre17 \ --spring.cloud.config.server.git.uri=https://github.com/spring-cloud-samples/config-repo diff --git a/docs/integrations/aiohttp.md b/docs/integrations/aiohttp.md index 765a44e..f727104 100644 --- a/docs/integrations/aiohttp.md +++ b/docs/integrations/aiohttp.md @@ -4,10 +4,13 @@ ### option 1: using environment variables -First run: export `APP_NAME=foo`, `export LABEL=main` +!!! tip "" + First run: + + - `export APP_NAME=foo` + - `export LABEL=main` - -``` py title="aiohttp_example_1.py" +``` py title="aiohttp-example-1.py" linenums="1" import logging from config.ext import AioHttpConfig @@ -57,7 +60,7 @@ web.run_app(app) ### option 2: using custom client -``` py title="aiohttp_example_2.py" +``` py title="aiohttp-example-2.py" linenums="1" import logging from config import ConfigClient @@ -111,7 +114,7 @@ web.run_app(app) ### option 3: using custom settings -``` py title="aiohttp_example_3.py" +``` py title="aiohttp-example-3.py" linenums="1" import logging from config import ConfigClient from config.ext import AioHttpConfig @@ -166,13 +169,12 @@ web.run_app(app) ## Using the CloudFoundry client -Set `APP_NAME` environment variable, for example. - -For details, see: [https://config-client.amenezes.net/docs/1.-overview/#setup](https://config-client.amenezes.net/docs/1.-overview/#setup) +!!! tip "" + First set `APP_NAME` environment variable ### option 1: using environment variables -``` py title="aiohttp_cf_example_1.py" +``` py title="aiohttp-cf-example-1.py" linenums="1" import logging from config import CF @@ -211,7 +213,7 @@ web.run_app(app) ### option 2: using custom client -``` py title="aiohttp_cf_example_2.py" +``` py title="aiohttp-cf-example-2.py" linenums="1" import logging from config import CF, ConfiClient @@ -250,7 +252,7 @@ web.run_app(app) ### option 3: using custom settings -``` py title="aiohttp_cf_example_3.py" +``` py title="aiohttp-cf-example-3.py" linenums="1" import logging from config import CF, ConfiClient diff --git a/docs/integrations/fastapi.md b/docs/integrations/fastapi.md index 1d8d40b..c97ed34 100644 --- a/docs/integrations/fastapi.md +++ b/docs/integrations/fastapi.md @@ -11,14 +11,14 @@ Why this approach? ### option 1: using environment variables -First run: +!!! tip "" + First run: + + - `export APP_NAME=foo` + - `export LABEL=main` -``` bash title="terminal" -export APP_NAME=foo -export LABEL=main -``` -``` py title="fastapi_example_1.py" +``` py title="fastapi-example-1.py" import logging from fastapi import Depends, FastAPI, Request @@ -70,13 +70,12 @@ def config(request: Request, config_key): ### option 1: using environment variables -Set `APP_NAME` environment variable as others if necessary. +!!! tip "" + Frist set `APP_NAME` environment variable as others if necessary. -``` bash title="terminal" -export APP_NAME=app_name -``` + For example: -For details, see: [https://config-client.amenezes.net/docs/1.-overview/#setup](https://config-client.amenezes.net/docs/1.-overview/#setup) + - `export APP_NAME=app_name` ``` py title="fastapi_cf_example_1.py" import logging diff --git a/docs/integrations/flask.md b/docs/integrations/flask.md index 5000b3d..2c03d60 100644 --- a/docs/integrations/flask.md +++ b/docs/integrations/flask.md @@ -4,14 +4,14 @@ ### option 1: using environment variables -First run: +!!! tip "" -``` bash title="terminal" -export APP_NAME=foo -export LABEL=main -``` + First run: + + - `export APP_NAME=foo` + - `export LABEL=main` -``` py title="flask_example_1.py" +``` py title="flask-example-1.py" import logging from config.ext.flask import FlaskConfig @@ -59,7 +59,7 @@ def config(key): ### option 2: using custom client -``` py title="flask_example_2.py" +``` py title="flask-example-2.py" import logging from config import ConfigClient @@ -106,7 +106,7 @@ def config(key): ### option 3: using custom settings -``` py title="flask_example_3.py" +``` py title="flask-example-3.py" import logging from config import ConfigClient @@ -159,13 +159,13 @@ def config(key): ### option 1: using environment variables -Setting `APP_NAME` environment variable as others if necessary. +!!! tip "" -``` bash title="terminal" -export APP_NAME=app_name -``` + First it's necesseary setting `APP_NAME` environment variable as others if necessary. -For details, see: [https://config-client.amenezes.net/docs/1.-overview/#setup](https://config-client.amenezes.net/docs/1.-overview/#setup) + For example: + + - `export APP_NAME=app_name` ``` py title="flask_cf_example_1.py" import logging diff --git a/docs/overview.md b/docs/overview.md index 273ee02..d236381 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,12 +1,18 @@ -# Introduction +Python config-client is a library for integration and usage with Spring Cloud Config. -The needed configuration to use `config-client` can be set through environment variables or parameters. +## Usage + +The needed configuration to use config-client can be set through: + +- environment variables; or +- arguments. ## Using environment variables -``` ini title=".env" -# default environment variables values: -# +The following presents the default values: + + +``` ini linenums="1" CONFIGSERVER_ADDRESS=http://localhost:8888 LABEL=master PROFILE=development @@ -14,40 +20,32 @@ APP_NAME= # empyt string CONFIG_FAIL_FAST=True ``` -If `fail_fast` it's enabled, by default, any error to contact server will raise `SystemExit(1)` otherwise `ConnectionError` will raised. +!!! tip "" -In the version `1.0.0` there's no more url property to customize request URL to get configuration from server, because merge files occur in the client side. Unfortunately the config-server API have a strange behavior and seems that no honor your API. - -### Content-Type supported for configuration - -- JSON `only` + By default, the value of the environment variable **`CONFIG_FAIL_FAST`** is set to **`True`**, so if any error occurs while contacting the server, a `SystemExit(1)` exception will be raised; otherwise, a `ConnectionError` exception is raised. ## Security -The `config-client` can interact with the Spring Cloud Config [encryption and decryption](https://cloud.spring.io/spring-cloud-config/reference/html/#_encryption_and_decryption) API. +The config-client can interact with the Spring Cloud Config [encryption and decryption API](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_encryption_and_decryption). ### [Encryption](https://config-client.amenezes.net/reference/config/spring/#encrypt) -#### Usage - -```python +``` py linenums="1" title="example-encryption.py" from config import ConfigClient client = ConfigClient() -my_secret = client.encrypt('my_secret') # returns a str -print(my_secret) +client.encrypt('my_secret') # for example: a3c3333956a1ab2ade8b8219e36d0a4cb97d9a2789cbbcd858ea4ef3130563c6 ``` ### [Decryption](https://config-client.amenezes.net/reference/config/spring/#decrypt) -#### Usage - -```python +``` py linenums="1" title="example-decryption.py" from config import ConfigClient client = ConfigClient() -my_secret = client.decrypt('a3c3333956a1ab2ade8b8219e36d0a4cb97d9a2789cbbcd858ea4ef3130563c6') # returns a str -print(my_secret) +client.decrypt( + 'a3c3333956a1ab2ade8b8219e36d0a4cb97d9a2789cbbcd858ea4ef3130563c6' +) # for example: my_secret ``` diff --git a/mkdocs.yml b/mkdocs.yml index b13da26..97f31df 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,20 +6,21 @@ theme: logo: spring-icon.png favicon: spring-icon.png features: + - toc.follow + - toc.integrate - navigation.instant - navigation.top - navigation.prune - - toc.integrate - search.highlight - search.suggest - search.share - content.code.annotate - content.tooltips - - toc.follow + - content.code.copy palette: - scheme: default primary: green - accent: teal + accent: light green toggle: icon: material/lightbulb-on name: Switch to dark mode @@ -39,6 +40,20 @@ extra: link: https://github.com/amenezes/config-client/issues - icon: fontawesome/solid/envelope link: mailto:alexandre.fmenezes@gmail.com +markdown_extensions: + - admonition + - pymdownx.details + - pymdownx.superfences + - tables + - attr_list + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format nav: - Overview: overview.md - Client: @@ -53,9 +68,3 @@ nav: - FastAPI: integrations/fastapi.md - CLI: command-line.md - Development: development.md -markdown_extensions: - - pymdownx.highlight: - use_pygments: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences diff --git a/requirements-dev.txt b/requirements-dev.txt index 27cc1bf..e11672e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,6 +16,7 @@ requests-mock pre-commit radon bandit +mkdocs-material # flask integration Flask # aiohttp integration From 07819987cd0587cc800485f2f662c24ffcc420f3 Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Tue, 24 Oct 2023 11:24:14 -0300 Subject: [PATCH 8/9] Updated CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1412335..e6acd32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,7 @@ jobs: path: "./requirements-dev.txt" - name: Run lint & tests run: | + pip install cython && \ make ci SKIP_STYLE=true - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 From 367a5b39a6d54c0760fab41e2d966e72484925bb Mon Sep 17 00:00:00 2001 From: Alexandre Menezes Date: Tue, 24 Oct 2023 11:35:09 -0300 Subject: [PATCH 9/9] Python 3.12 disabled from CI due to aiohttp error --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6acd32..c298020 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ jobs: tests: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.8', '3.9', '3.10', '3.11'] os: [ubuntu] fail-fast: true runs-on: ${{ matrix.os }}-latest @@ -29,7 +29,6 @@ jobs: path: "./requirements-dev.txt" - name: Run lint & tests run: | - pip install cython && \ make ci SKIP_STYLE=true - name: Upload coverage to Codecov uses: codecov/codecov-action@v1