diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 4aa7446..a8d4a0d 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -176,7 +176,11 @@ jobs: release: name: "🚀 GitHub Release" runs-on: ubuntu-latest - needs: [build-macos, build-python-wheel, build-ubuntu, build-windows] + needs: + - build-macos + - build-python-wheel + - build-ubuntu + - build-windows if: startsWith(github.ref, 'refs/tags/') @@ -208,6 +212,7 @@ jobs: - name: Release uses: softprops/action-gh-release@v2 with: + discussion_category_name: announcements fail_on_unmatched_files: true files: builds/**/* generate_release_notes: true @@ -215,7 +220,13 @@ jobs: release-pypi: name: "🐍 Release on PyPI" runs-on: ubuntu-latest - needs: [build-python-wheel] + needs: + - build-python-wheel + environment: + name: pypi + url: https://pypi.org/project/geotribu/ + permissions: + id-token: write if: startsWith(github.ref, 'refs/tags/') @@ -230,12 +241,15 @@ jobs: if: startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.PYPI_API_TOKEN }} + packages-dir: builds/wheel + print-hash: true release-ghcr: name: "🐳 Release as Docker container" runs-on: ubuntu-latest - needs: [release, release-pypi] + needs: + - release + - release-pypi env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 80f43ff..b8a3d84 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,5 @@ { // Editor - "editor.rulers": [ - 88 - ], - "editor.wordWrapColumn": 88, "editor.bracketPairColorization.enabled": true, "editor.guides.bracketPairs": "active", "files.associations": { @@ -11,18 +7,31 @@ }, // JSON "[json]": { + "editor.defaultFormatter": "vscode.json-language-features", "editor.formatOnSave": true, "editor.quickSuggestions": { "strings": true }, "editor.suggest.insertMode": "replace" }, + // Markdown + "markdown.updateLinksOnFileMove.enabled": "prompt", + "markdown.updateLinksOnFileMove.enableForDirectories": true, + "markdown.validate.enabled": true, + "markdown.validate.fileLinks.markdownFragmentLinks": "warning", + "markdown.validate.fragmentLinks.enabled": "warning", + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "editor.formatOnSave": true, + "files.trimTrailingWhitespace": false, + }, // Python "python.analysis.autoFormatStrings": true, "python.analysis.typeCheckingMode": "basic", - "python.analysis.autoImportCompletions": true, "python.terminal.activateEnvInCurrentTerminal": true, "python.terminal.activateEnvironment": true, + "python.testing.unittestEnabled": true, + "python.testing.pytestEnabled": true, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter", "editor.formatOnSave": true, @@ -34,9 +43,6 @@ ], "editor.wordWrapColumn": 88, }, - // Tests - "python.testing.unittestEnabled": true, - "python.testing.pytestEnabled": true, // YAML "[yaml]": { "editor.autoIndent": "keep", @@ -58,4 +64,12 @@ "--config=setup.cfg", "--verbose" ], + "markdownlint.config": { + "MD007": { + "indent": 4 + }, + "MD024": { + "allow_different_nesting": true + }, + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8850cae..58635f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,22 @@ Unreleased --> +## 0.33.0 - 2024-05-02 + +### Features and enhancements 🎉 + +* refacto: use mastodonpy to broadcast comments by @Guts in +* Améliore la gestion des logs by @Guts in + +### Tooling 🔧 + +* Ignore .idea folder by @gounux in +* ci: limit parallel tests to 2 by @Guts in + +## New Contributors + +* @gounux made their first contribution in + ## 0.32.1 - 2024-03-09 ### Bugs fixes 🐛 diff --git a/docs/conf.py b/docs/conf.py index 4263ea4..e362445 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,6 +23,7 @@ description = __about__.__summary__ project = __about__.__title__ version = release = __about__.__version__ +github_doc_root = f"{__about__.__uri__}/tree/main/docs/" # -- General configuration --------------------------------------------------- @@ -170,7 +171,7 @@ "deflist", "html_admonition", "html_image", - # "linkify", + "linkify", "replacements", "smartquotes", "strikethrough", diff --git a/geotribu_cli/__about__.py b/geotribu_cli/__about__.py index 070a76c..6f0f434 100644 --- a/geotribu_cli/__about__.py +++ b/geotribu_cli/__about__.py @@ -38,7 +38,7 @@ __uri__ = __uri_repository__ -__version__ = "0.32.1" +__version__ = "0.33.0" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/geotribu_cli/cli.py b/geotribu_cli/cli.py index a26b0a1..dff08db 100644 --- a/geotribu_cli/cli.py +++ b/geotribu_cli/cli.py @@ -38,6 +38,7 @@ parser_search_image, parser_upgrade, ) +from geotribu_cli.utils.journalizer import configure_logger # ############################################################################# # ########## Globals ############### @@ -144,11 +145,18 @@ def main(args: list[str] = None): action="count", default=1, dest="verbosity", - # metavar="GEOTRIBU_LOGS_LEVEL", help="Niveau de verbosité : None = WARNING, -v = INFO, -vv = DEBUG. Réglable " "avec la variable d'environnement GEOTRIBU_LOGS_LEVEL.", ) + main_parser.add_argument( + "--no-logfile", + default=True, + action="store_false", + dest="opt_logfile_disabled", + help="Désactiver les fichiers de journalisation (logs).", + ) + main_parser.add_argument( "-h", "--help", @@ -351,23 +359,13 @@ def main(args: list[str] = None): # just get passed args args = main_parser.parse_args(args) - # set log level depending on verbosity argument - if 0 < args.verbosity < 4: - args.verbosity = 40 - (10 * args.verbosity) - elif args.verbosity >= 4: - # debug is the limit - args.verbosity = 40 - (10 * 3) + # log configuration + if args.opt_logfile_disabled: + configure_logger( + verbosity=args.verbosity, logfile=f"{__title_clean__}_{__version__}.log" + ) else: - args.verbosity = 0 - - logging.basicConfig( - level=args.verbosity, - format="%(asctime)s||%(levelname)s||%(module)s||%(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - console = logging.StreamHandler() - console.setLevel(args.verbosity) + configure_logger(verbosity=args.verbosity) # add the handler to the root logger logger = logging.getLogger(__title_clean__) diff --git a/geotribu_cli/utils/journalizer.py b/geotribu_cli/utils/journalizer.py new file mode 100644 index 0000000..3347f87 --- /dev/null +++ b/geotribu_cli/utils/journalizer.py @@ -0,0 +1,172 @@ +#! python3 # noqa: E265 + +"""Helper to configure logging depending on CLI options.""" + +# ############################################################################ +# ########## IMPORTS ############# +# ################################ + +# standard library +import logging +from getpass import getuser +from logging.handlers import RotatingFileHandler +from os import environ, getenv +from os.path import expanduser, expandvars +from pathlib import Path +from platform import architecture, platform +from socket import gethostname +from typing import Optional + +# package +from geotribu_cli.__about__ import __title__, __version__ +from geotribu_cli.constants import GeotribuDefaults +from geotribu_cli.utils.check_path import check_path +from geotribu_cli.utils.proxies import get_proxy_settings +from geotribu_cli.utils.str2bool import str2bool + +# ############################################################################ +# ########## GLOBALS ############# +# ################################ + +# logs +logger = logging.getLogger(__name__) +default_settings = GeotribuDefaults() + +# ############################################################################ +# ########## FUNCTIONS ########### +# ################################ + + +def configure_logger(verbosity: int = 1, logfile: Optional[Path] = None): + """Configure logging according to verbosity from CLI. + + Args: + verbosity (int): verbosity level + logfile (Path, optional): file where to store log. Defaults to None. + """ + # handle log level overridden by environment variable + verbosity = getenv("GEOTRIBU_LOGS_LEVEL", verbosity) + try: + verbosity = int(verbosity) + except ValueError as err: + logger.error(f"Bad verbosity value type: {err}. Fallback to 1.") + verbosity = 1 + + # set log level depending on verbosity argument + if 0 < verbosity < 4: + verbosity = 40 - (10 * verbosity) + elif verbosity >= 4: + # debug is the limit + verbosity = 40 - (10 * 3) + else: + verbosity = 0 + + # set console handler + log_console_handler = logging.StreamHandler() + log_console_handler.setLevel(verbosity) + + # set log file + if not logfile: + logging.basicConfig( + level=verbosity, + format="%(asctime)s||%(levelname)s||%(module)s||%(funcName)s||%(lineno)d||%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[log_console_handler], + ) + + else: + if getenv("GEOTRIBU_LOGS_DIR") and check_path( + input_path=Path(expandvars(expanduser(getenv("GEOTRIBU_LOGS_DIR")))), + must_be_a_file=False, + must_be_a_folder=True, + must_be_writable=True, + raise_error=False, + ): + logs_folder = Path(expandvars(expanduser(getenv("GEOTRIBU_LOGS_DIR")))) + logger.debug( + f"Logs folder set with GEOTRIBU_LOGS_DIR environment variable: {logs_folder}" + ) + else: + logs_folder: Path = default_settings.geotribu_working_folder.joinpath( + "logs" + ) + logger.debug( + "Logs folder specified in GEOTRIBU_LOGS_DIR environment variable " + f"{getenv('GEOTRIBU_LOGS_DIR')} can't be used (see logs above). Fallback on " + f"default folder: {logs_folder}" + ) + + # make sure folder exists + logs_folder.mkdir(exist_ok=True, parents=True) + logs_filepath = Path(logs_folder, logfile) + + log_file_handler = RotatingFileHandler( + backupCount=10, + delay=True, + encoding="UTF-8", + filename=logs_filepath, + maxBytes=3000000, + mode="a", + ) + # force new file by execution + if logs_filepath.is_file(): + log_file_handler.doRollover() + + logging.basicConfig( + level=verbosity, + format="%(asctime)s||%(levelname)s||%(module)s||%(funcName)s||%(lineno)d||%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + handlers=[log_console_handler, log_file_handler], + ) + + logger.info(f"Log file: {logs_filepath}") + + headers() + + +def headers(): + """Basic information to log before other message.""" + # initialize the log + logger.info(f"{'='*10} {__title__} - {__version__} {'='*10}") + logger.debug(f"Operating System: {platform()}") + logger.debug(f"Architecture: {architecture()[0]}") + logger.debug(f"Computer: {gethostname()}") + logger.debug(f"Launched by user: {getuser()}") + + if getenv("userdomain"): + logger.debug(f"OS Domain: {getenv('userdomain')}") + + if get_proxy_settings(): + logger.debug(f"Network proxies detected: {get_proxy_settings()}") + else: + logger.debug("No network proxies detected") + + if str2bool(getenv("QDT_SSL_USE_SYSTEM_STORES", False)): + logger.debug("Option to use native system certificates stores is enabled.") + if "REQUESTS_CA_BUNDLE" in environ: + environ.pop("REQUESTS_CA_BUNDLE") + logger.debug( + "Custom path to CA Bundle (REQUESTS_CA_BUNDLE) has been removed from " + "environment variables." + ) + if "CURL_CA_BUNDLE" in environ: + environ.pop("CURL_CA_BUNDLE") + logger.debug( + "Custom path to CA Bundle (CURL_CA_BUNDLE) has been removed from " + "environment variables." + ) + + +def get_logger_filepath() -> Optional[Path]: + """Retrieve log filepath within logger handlers. + + Returns: + Path | None: path to the logfile or None if no handler has baseFilename attr. + """ + if logger.root.hasHandlers(): + for handler in logger.root.handlers: + if hasattr(handler, "baseFilename"): + return Path(handler.baseFilename) + + logger.warning("No file found in ay log handlers.") + return None diff --git a/requirements/base.txt b/requirements/base.txt index c06d3f8..30f99dd 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ markdownify>=0.11,<0.13 Mastodon.py>=1.8.1,<1.9 orjson>=3.8,<3.11 packaging>=20,<25 -rich_argparse>=0.6,<1.5 +rich_argparse>=1,<1.5 python-frontmatter>=1,<2 requests>=2.31,<3 typing-extensions>=4,<5 ; python_version < '3.11' diff --git a/setup.cfg b/setup.cfg index c60d78c..84feb0d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,11 @@ norecursedirs = .* build dev development dist docs CVS fixtures _darcs {arch} *. python_files = test_*.py testpaths = tests +[tool.pytest.ini_options] +retries = 2 +retry_delay = 0.5 +cumulative_timing = false + [coverage:run] branch = True omit = diff --git a/setup.py b/setup.py index 1dd193d..d22a57d 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def load_requirements(requirements_files: Union[Path, list[Path]]) -> list: long_description=README, long_description_content_type="text/markdown", keywords=__about__.__keywords__, - url=__about__.__uri__, + url=__about__.__uri_homepage__, project_urls={ "Docs": __about__.__uri_homepage__, "Bug Reports": __about__.__uri_tracker__, diff --git a/tests/test_cli.py b/tests/test_cli.py index ade07a8..3c1f0c6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -78,10 +78,21 @@ def test_cli_run_comments_open(capsys): assert Path(Path().home() / ".geotribu/comments/latest.json").exists() -@pytest.mark.flaky(retries=3, delay=1, only_on=[SystemExit]) +@pytest.mark.flaky(retries=3, delay=5, only_on=[SystemExit]) def test_cli_run_comments_open_specific(capsys): """Test nested subcommand comments latest.""" - cli.main(["comments", "open", "--page-size", "25", "--comment-id", "15"]) + cli.main( + [ + "comments", + "open", + "--page-size", + "25", + "--comment-id", + "15", + "--expiration-rotating-hours", + "0", + ] + ) out, err = capsys.readouterr() diff --git a/tests/test_mastodon_client.py b/tests/test_mastodon_client.py index 1ca4508..2869bde 100644 --- a/tests/test_mastodon_client.py +++ b/tests/test_mastodon_client.py @@ -10,6 +10,7 @@ # standard import unittest +from os import getenv from pathlib import Path from tempfile import TemporaryDirectory @@ -17,9 +18,6 @@ from geotribu_cli.__about__ import __title_clean__, __version__ from geotribu_cli.social.mastodon_client import ExtendedMastodonClient -# 3rd party - - # ############################################################################ # ########## Classes ############# # ################################ @@ -58,6 +56,10 @@ def test_instance_domain_from_url(self): "mapstodon.space", ) + @unittest.skipIf( + condition=getenv("GEOTRIBU_MASTODON_API_ACCESS_TOKEN") is None, + reason="Le jeton d'API Mastodon est requis pour exécuter ce test.", + ) def test_export_data_all(self): """Test export following accounts to CSV.""" masto_client = ExtendedMastodonClient(