From 5d2779d03bceee7bb25f3768e49e702b37adc981 Mon Sep 17 00:00:00 2001 From: Yannick Payot Date: Wed, 24 May 2023 14:48:02 +0200 Subject: [PATCH 1/2] Change CI to GitHub actions Use copier template from oca/oca-addons-repo-template Apply linting --- .copier-answers.yml | 27 +++ .editorconfig | 20 ++ .eslintrc.yml | 187 +++++++++++++++ .flake8 | 12 + .github/workflows/pre-commit.yml | 35 +++ .github/workflows/stale.yml | 69 ++++++ .github/workflows/test.yml | 96 ++++++++ .gitignore | 23 +- .isort.cfg | 13 ++ .pre-commit-config.yaml | 133 +++++++++++ .prettierrc.yml | 8 + .pylintrc | 122 ++++++++++ .pylintrc-mandatory | 97 ++++++++ .travis.yml | 44 ---- LICENSE | 12 +- README.md | 31 ++- attachment_azure/models/ir_attachment.py | 53 ++--- attachment_s3/__init__.py | 1 - attachment_s3/__manifest__.py | 29 +-- attachment_s3/models/__init__.py | 1 - attachment_s3/models/ir_attachment.py | 109 ++++----- attachment_swift/__manifest__.py | 36 +-- attachment_swift/models/ir_attachment.py | 89 ++++---- attachment_swift/swift_uri.py | 3 +- attachment_swift/tests/__init__.py | 1 - attachment_swift/tests/test_mock_swift_api.py | 116 +++++----- .../tests/test_with_swift_store.py | 26 +-- .../__manifest__.py | 25 +- .../data/res_config_settings_data.xml | 6 +- .../models/ir_attachment.py | 214 +++++++++--------- base_fileurl_field/__init__.py | 1 - base_fileurl_field/__manifest__.py | 1 + base_fileurl_field/fields.py | 40 ++-- cloud_platform/__init__.py | 1 - cloud_platform/__manifest__.py | 33 +-- cloud_platform/models/__init__.py | 1 - cloud_platform/models/cloud_platform.py | 74 +++--- cloud_platform/songs.py | 3 +- cloud_platform_azure/README.md | 3 +- cloud_platform_azure/__manifest__.py | 2 +- cloud_platform_azure/models/cloud_platform.py | 13 +- cloud_platform_exoscale/README.md | 3 +- cloud_platform_exoscale/__manifest__.py | 2 +- .../models/cloud_platform.py | 55 ++--- cloud_platform_ovh/README.md | 4 +- cloud_platform_ovh/__manifest__.py | 2 +- cloud_platform_ovh/models/cloud_platform.py | 57 +++-- logging_json/__init__.py | 1 - logging_json/__manifest__.py | 30 +-- logging_json/json_log.py | 14 +- monitoring_log_requests/__init__.py | 1 - monitoring_log_requests/__manifest__.py | 21 +- monitoring_log_requests/models/__init__.py | 1 - monitoring_log_requests/models/ir_http.py | 77 ++++--- monitoring_prometheus/__manifest__.py | 2 +- .../controllers/prometheus_metrics.py | 5 +- monitoring_prometheus/models/ir_http.py | 4 +- monitoring_statsd/__init__.py | 1 - monitoring_statsd/__manifest__.py | 34 +-- monitoring_statsd/models/__init__.py | 1 - monitoring_statsd/models/ir_http.py | 50 ++-- monitoring_statsd/statsd_client.py | 42 ++-- monitoring_status/__manifest__.py | 21 +- monitoring_status/controllers/main.py | 17 +- session_redis/__init__.py | 1 - session_redis/__manifest__.py | 29 +-- session_redis/http.py | 63 +++--- session_redis/json_encoding.py | 1 - session_redis/session.py | 70 +++--- test_base_fileurl_field/__manifest__.py | 5 +- test_base_fileurl_field/data/sample.txt | 2 +- test_base_fileurl_field/models/res_partner.py | 46 ++-- test_base_fileurl_field/models/res_users.py | 8 +- .../tests/ir_attachment.py | 14 +- .../tests/test_fileurl_fields.py | 51 +++-- test_base_fileurl_field/views/res_partner.xml | 12 +- test_base_fileurl_field/views/res_users.xml | 4 +- 77 files changed, 1697 insertions(+), 864 deletions(-) create mode 100644 .copier-answers.yml create mode 100644 .editorconfig create mode 100644 .eslintrc.yml create mode 100644 .flake8 create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .github/workflows/test.yml create mode 100644 .isort.cfg create mode 100644 .pre-commit-config.yaml create mode 100644 .prettierrc.yml create mode 100644 .pylintrc create mode 100644 .pylintrc-mandatory delete mode 100644 .travis.yml diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 00000000..84077995 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,27 @@ +# Do NOT update manually; changes here will be overwritten by Copier +_commit: v1.14.2 +_src_path: https://github.com/OCA/oca-addons-repo-template.git +ci: GitHub +dependency_installation_mode: PIP +generate_requirements_txt: false +github_check_license: true +github_ci_extra_env: {} +github_enable_codecov: true +github_enable_makepot: false +github_enable_stale_action: true +github_enforce_dev_status_compatibility: false +include_wkhtmltopdf: false +odoo_version: 15.0 +org_name: Camptocamp +org_slug: camptocamp +rebel_module_groups: +- attachment_s3,cloud_platform_exoscale +- attachment_swift,cloud_platform_ovh +- attachment_azure,cloud_platform_azure +repo_description: '' +repo_name: Odoo Cloud Addons +repo_slug: odoo-cloud-platform +repo_website: https://github.com/camptocamp/odoo-cloud-platform +travis_apt_packages: [] +travis_apt_sources: [] + diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..bfd7ac53 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Configuration for known file extensions +[*.{css,js,json,less,md,py,rst,sass,scss,xml,yaml,yml}] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{json,yml,yaml,rst,md}] +indent_size = 2 + +# Do not configure editor for libs and autogenerated content +[{*/static/{lib,src/lib}/**,*/static/description/index.html,*/readme/../README.rst}] +charset = unset +end_of_line = unset +indent_size = unset +indent_style = unset +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..9429bc68 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,187 @@ +env: + browser: true + es6: true + +# See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 +parserOptions: + ecmaVersion: 2019 + +overrides: + - files: + - "**/*.esm.js" + parserOptions: + sourceType: module + +# Globals available in Odoo that shouldn't produce errorings +globals: + _: readonly + $: readonly + fuzzy: readonly + jQuery: readonly + moment: readonly + odoo: readonly + openerp: readonly + owl: readonly + +# Styling is handled by Prettier, so we only need to enable AST rules; +# see https://github.com/OCA/maintainer-quality-tools/pull/618#issuecomment-558576890 +rules: + accessor-pairs: warn + array-callback-return: warn + callback-return: warn + capitalized-comments: + - warn + - always + - ignoreConsecutiveComments: true + ignoreInlineComments: true + complexity: + - warn + - 15 + constructor-super: warn + dot-notation: warn + eqeqeq: warn + global-require: warn + handle-callback-err: warn + id-blacklist: warn + id-match: warn + init-declarations: error + max-depth: warn + max-nested-callbacks: warn + max-statements-per-line: warn + no-alert: warn + no-array-constructor: warn + no-caller: warn + no-case-declarations: warn + no-class-assign: warn + no-cond-assign: error + no-const-assign: error + no-constant-condition: warn + no-control-regex: warn + no-debugger: error + no-delete-var: warn + no-div-regex: warn + no-dupe-args: error + no-dupe-class-members: error + no-dupe-keys: error + no-duplicate-case: error + no-duplicate-imports: error + no-else-return: warn + no-empty-character-class: warn + no-empty-function: error + no-empty-pattern: error + no-empty: warn + no-eq-null: error + no-eval: error + no-ex-assign: error + no-extend-native: warn + no-extra-bind: warn + no-extra-boolean-cast: warn + no-extra-label: warn + no-fallthrough: warn + no-func-assign: error + no-global-assign: error + no-implicit-coercion: + - warn + - allow: ["~"] + no-implicit-globals: warn + no-implied-eval: warn + no-inline-comments: warn + no-inner-declarations: warn + no-invalid-regexp: warn + no-irregular-whitespace: warn + no-iterator: warn + no-label-var: warn + no-labels: warn + no-lone-blocks: warn + no-lonely-if: error + no-mixed-requires: error + no-multi-str: warn + no-native-reassign: error + no-negated-condition: warn + no-negated-in-lhs: error + no-new-func: warn + no-new-object: warn + no-new-require: warn + no-new-symbol: warn + no-new-wrappers: warn + no-new: warn + no-obj-calls: warn + no-octal-escape: warn + no-octal: warn + no-param-reassign: warn + no-path-concat: warn + no-process-env: warn + no-process-exit: warn + no-proto: warn + no-prototype-builtins: warn + no-redeclare: warn + no-regex-spaces: warn + no-restricted-globals: warn + no-restricted-imports: warn + no-restricted-modules: warn + no-restricted-syntax: warn + no-return-assign: error + no-script-url: warn + no-self-assign: warn + no-self-compare: warn + no-sequences: warn + no-shadow-restricted-names: warn + no-shadow: warn + no-sparse-arrays: warn + no-sync: warn + no-this-before-super: warn + no-throw-literal: warn + no-undef-init: warn + no-undef: error + no-unmodified-loop-condition: warn + no-unneeded-ternary: error + no-unreachable: error + no-unsafe-finally: error + no-unused-expressions: error + no-unused-labels: error + no-unused-vars: error + no-use-before-define: error + no-useless-call: warn + no-useless-computed-key: warn + no-useless-concat: warn + no-useless-constructor: warn + no-useless-escape: warn + no-useless-rename: warn + no-void: warn + no-with: warn + operator-assignment: [error, always] + prefer-const: warn + radix: warn + require-yield: warn + sort-imports: warn + spaced-comment: [error, always] + strict: [error, function] + use-isnan: error + valid-jsdoc: + - warn + - prefer: + arg: param + argument: param + augments: extends + constructor: class + exception: throws + func: function + method: function + prop: property + return: returns + virtual: abstract + yield: yields + preferType: + array: Array + bool: Boolean + boolean: Boolean + number: Number + object: Object + str: String + string: String + requireParamDescription: false + requireReturn: false + requireReturnDescription: false + requireReturnType: false + valid-typeof: warn + yoda: warn diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..e397e8ed --- /dev/null +++ b/.flake8 @@ -0,0 +1,12 @@ +[flake8] +max-line-length = 88 +max-complexity = 16 +# B = bugbear +# B9 = bugbear opinionated (incl line length) +select = C,E,F,W,B,B9 +# E203: whitespace before ':' (black behaviour) +# E501: flake8 line length (covered by bugbear B950) +# W503: line break before binary operator (black behaviour) +ignore = E203,E501,W503 +per-file-ignores= + __init__.py:F401 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..6973a9b5 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,35 @@ +name: pre-commit + +on: + pull_request: + branches: + - "15.0*" + push: + branches: + - "15.0" + - "15.0-ocabot-*" + +jobs: + pre-commit: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v2 + - name: Get python version + run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV + - uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + - name: Install pre-commit + run: pip install pre-commit + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure --color=always + - name: Check that all files generated by pre-commit are in git + run: | + newfiles="$(git ls-files --others --exclude-from=.gitignore)" + if [ "$newfiles" != "" ] ; then + echo "Please check-in the following files:" + echo "$newfiles" + exit 1 + fi diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..1693a125 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,69 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: "0 12 * * 0" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - name: Stale PRs and issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # General settings. + ascending: true + remove-stale-when-updated: true + # Pull Requests settings. + # 120+30 day stale policy for PRs + # * Except PRs marked as "no stale" + days-before-pr-stale: 120 + days-before-pr-close: 30 + exempt-pr-labels: "no stale" + stale-pr-label: "stale" + stale-pr-message: > + There hasn't been any activity on this pull request in the past 4 months, so + it has been marked as stale and it will be closed automatically if no + further activity occurs in the next 30 days. + + If you want this PR to never become stale, please ask a PSC member to apply + the "no stale" label. + # Issues settings. + # 180+30 day stale policy for open issues + # * Except Issues marked as "no stale" + days-before-issue-stale: 180 + days-before-issue-close: 30 + exempt-issue-labels: "no stale,needs more information" + stale-issue-label: "stale" + stale-issue-message: > + There hasn't been any activity on this issue in the past 6 months, so it has + been marked as stale and it will be closed automatically if no further + activity occurs in the next 30 days. + + If you want this issue to never become stale, please ask a PSC member to + apply the "no stale" label. + + # 15+30 day stale policy for issues pending more information + # * Issues that are pending more information + # * Except Issues marked as "no stale" + - name: Needs more information stale issues policy + uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + ascending: true + only-labels: "needs more information" + exempt-issue-labels: "no stale" + days-before-stale: 15 + days-before-close: 30 + days-before-pr-stale: -1 + days-before-pr-close: -1 + remove-stale-when-updated: true + stale-issue-label: "stale" + stale-issue-message: > + This issue needs more information and there hasn't been any activity + recently, so it has been marked as stale and it will be closed automatically + if no further activity occurs in the next 30 days. + + If you think this is a mistake, please ask a PSC member to remove the "needs + more information" label. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..85025db7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,96 @@ +name: tests + +on: + pull_request: + branches: + - "15.0*" + push: + branches: + - "15.0" + - "15.0-ocabot-*" + +jobs: + unreleased-deps: + runs-on: ubuntu-latest + name: Detect unreleased dependencies + steps: + - uses: actions/checkout@v3 + - run: | + for reqfile in requirements.txt test-requirements.txt ; do + if [ -f ${reqfile} ] ; then + result=0 + # reject non-comment lines that contain a / (i.e. URLs, relative paths) + grep "^[^#].*/" ${reqfile} || result=$? + if [ $result -eq 0 ] ; then + echo "Unreleased dependencies found in ${reqfile}." + exit 1 + fi + fi + done + test: + runs-on: ubuntu-22.04 + container: ${{ matrix.container }} + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - container: ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest + include: "attachment_s3,cloud_platform_exoscale" + makepot: "false" + name: test exoscale S3 with Odoo + - container: ghcr.io/oca/oca-ci/py3.8-ocb15.0:latest + include: "attachment_s3,cloud_platform_exoscale" + name: test exoscale S3 with OCB + - container: ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest + include: "attachment_swift,cloud_platform_ovh" + makepot: "false" + name: test OVH with Odoo + - container: ghcr.io/oca/oca-ci/py3.8-ocb15.0:latest + include: "attachment_swift,cloud_platform_ovh" + name: test azure with OCB + - container: ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest + include: "attachment_azure,cloud_platform_azure" + makepot: "false" + name: test azure with Odoo + - container: ghcr.io/oca/oca-ci/py3.8-ocb15.0:latest + include: "attachment_azure,cloud_platform_azure" + name: test OVH with OCB + - container: ghcr.io/oca/oca-ci/py3.8-odoo15.0:latest + exclude: "attachment_s3,cloud_platform_exoscale,attachment_swift,cloud_platform_ovh,attachment_azure,cloud_platform_azure" + makepot: "false" + name: test others with Odoo + - container: ghcr.io/oca/oca-ci/py3.8-ocb15.0:latest + exclude: "attachment_s3,cloud_platform_exoscale,attachment_swift,cloud_platform_ovh,attachment_azure,cloud_platform_azure" + name: test others with Odoo + services: + postgres: + image: postgres:9.6 + env: + POSTGRES_USER: odoo + POSTGRES_PASSWORD: odoo + POSTGRES_DB: odoo + ports: + - 5432:5432 + env: + INCLUDE: "${{ matrix.include }}" + EXCLUDE: "${{ matrix.exclude }}" + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + - name: Install addons and dependencies + run: oca_install_addons + - name: Check licenses + run: manifestoo -d . check-licenses + - name: Check development status + run: manifestoo -d . check-dev-status --default-dev-status=Beta + continue-on-error: true + - name: Initialize test db + run: oca_init_test_database + - name: Run tests + run: oca_run_tests + - uses: codecov/codecov-action@v1 + - name: Update .pot files + run: oca_export_and_push_pot https://x-access-token:${{ secrets.GIT_PUSH_TOKEN }}@github.com/${{ github.repository }} + if: ${{ matrix.makepot == 'true' && github.event_name == 'push' && github.repository_owner == 'camptocamp' }} diff --git a/.gitignore b/.gitignore index 50b17c63..9c283fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +/.venv +/.pytest_cache # C extensions *.so @@ -13,8 +15,6 @@ build/ develop-eggs/ dist/ eggs/ -.eggs/ -lib/ lib64/ parts/ sdist/ @@ -22,6 +22,7 @@ var/ *.egg-info/ .installed.cfg *.egg +*.eggs # Installer logs pip-log.txt @@ -41,6 +42,19 @@ coverage.xml # Pycharm .idea +# Eclipse +.settings + +# Visual Studio cache/options directory +.vs/ +.vscode + +# OSX Files +.DS_Store + +# Django stuff: +*.log + # Mr Developer .mr.developer.cfg .project @@ -50,8 +64,11 @@ coverage.xml .ropeproject # Sphinx documentation -connector/doc/_build/ +docs/_build/ # Backup files *~ *.swp + +# OCA rules +!static/lib/ diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..0ec187ef --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,13 @@ +[settings] +; see https://github.com/psf/black +multi_line_output=3 +include_trailing_comma=True +force_grid_wrap=0 +combine_as_imports=True +use_parentheses=True +line_length=88 +known_odoo=odoo +known_odoo_addons=odoo.addons +sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER +default_section=THIRDPARTY +ensure_newline_before_comments = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..970386fa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,133 @@ +exclude: | + (?x) + # NOT INSTALLABLE ADDONS + ^test_base_fileurl_field/| + # END NOT INSTALLABLE ADDONS + # Files and folders generated by bots, to avoid loops + ^setup/|/static/description/index\.html$| + # We don't want to mess with tool-generated files + .svg$|/tests/([^/]+/)?cassettes/|^.copier-answers.yml$|^.github/| + # Maybe reactivate this when all README files include prettier ignore tags? + ^README\.md$| + # Library files can have extraneous formatting (even minimized) + /static/(src/)?lib/| + # Repos using Sphinx to generate docs don't need prettying + ^docs/_templates/.*\.html$| + # You don't usually want a bot to modify your legal texts + (LICENSE.*|COPYING.*) +default_language_version: + python: python3 + node: "14.18.0" +repos: + - repo: local + hooks: + # These files are most likely copier diff rejection junks; if found, + # review them manually, fix the problem (if needed) and remove them + - id: forbidden-files + name: forbidden files + entry: found forbidden files; remove them + language: fail + files: "\\.rej$" + - id: en-po-files + name: en.po files cannot exist + entry: found a en.po file + language: fail + files: '[a-zA-Z0-9_]*/i18n/en\.po$' + - repo: https://github.com/oca/maintainer-tools + rev: dfba427ba03900b69e0a7f2c65890dc48921d36a + hooks: + # update the NOT INSTALLABLE ADDONS section above + - id: oca-update-pre-commit-excluded-addons + - id: oca-fix-manifest-website + args: ["https://github.com/camptocamp/odoo-cloud-platform"] + - repo: https://github.com/myint/autoflake + rev: v1.4 + hooks: + - id: autoflake + args: + - --expand-star-imports + - --ignore-init-module-imports + - --in-place + - --remove-all-unused-imports + - --remove-duplicate-keys + - --remove-unused-variables + - repo: https://github.com/psf/black + rev: 22.3.0 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.1 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.4.1" + - "@prettier/plugin-xml@1.1.0" + args: + - --plugin=@prettier/plugin-xml + files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$ + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v7.32.0 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: end-of-file-fixer + # exclude autogenerated files + exclude: /README\.rst$|\.pot?$ + - id: debug-statements + - id: fix-encoding-pragma + args: ["--remove"] + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-merge-conflict + # exclude files where underlines are not distinguishable from merge conflicts + exclude: /README\.rst$|^docs/.*\.rst$ + - id: check-symlinks + - id: check-xml + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/asottile/pyupgrade + rev: v2.29.0 + hooks: + - id: pyupgrade + args: ["--keep-percent-format"] + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort except __init__.py + args: + - --settings=. + exclude: /__init__\.py$ + - repo: https://github.com/acsone/setuptools-odoo + rev: 3.1.8 + hooks: + - id: setuptools-odoo-make-default + - repo: https://github.com/PyCQA/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + name: flake8 + additional_dependencies: ["flake8-bugbear==21.9.2"] + - repo: https://github.com/OCA/pylint-odoo + rev: 7.0.2 + hooks: + - id: pylint_odoo + name: pylint with optional checks + args: + - --rcfile=.pylintrc + - --exit-zero + verbose: true + - id: pylint_odoo + args: + - --rcfile=.pylintrc-mandatory diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..5b6d4b36 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,8 @@ +# Defaults for all prettier-supported languages. +# Prettier will complete this with settings from .editorconfig file. +bracketSpacing: false +printWidth: 88 +proseWrap: always +semi: true +trailingComma: "es5" +xmlWhitespaceSensitivity: "strict" diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..f968b8f6 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,122 @@ + + +[MASTER] +load-plugins=pylint_odoo +score=n + +[ODOOLINT] +readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst" +manifest_required_authors=Camptocamp +manifest_required_keys=license +manifest_deprecated_keys=description,active +license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3 +valid_odoo_versions=15.0 + +[MESSAGES CONTROL] +disable=all + +# This .pylintrc contains optional AND mandatory checks and is meant to be +# loaded in an IDE to have it check everything, in the hope this will make +# optional checks more visible to contributors who otherwise never look at a +# green travis to see optional checks that failed. +# .pylintrc-mandatory containing only mandatory checks is used the pre-commit +# config as a blocking check. + +enable=anomalous-backslash-in-string, + api-one-deprecated, + api-one-multi-together, + assignment-from-none, + attribute-deprecated, + class-camelcase, + dangerous-default-value, + dangerous-view-replace-wo-priority, + development-status-allowed, + duplicate-id-csv, + duplicate-key, + duplicate-xml-fields, + duplicate-xml-record-id, + eval-referenced, + eval-used, + incoherent-interpreter-exec-perm, + license-allowed, + manifest-author-string, + manifest-deprecated-key, + manifest-required-author, + manifest-required-key, + manifest-version-format, + method-compute, + method-inverse, + method-required-super, + method-search, + openerp-exception-warning, + pointless-statement, + pointless-string-statement, + print-used, + redundant-keyword-arg, + redundant-modulename-xml, + reimported, + relative-import, + return-in-init, + rst-syntax-error, + sql-injection, + too-few-format-args, + translation-field, + translation-required, + unreachable, + use-vim-comment, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + attribute-string-redundant, + character-not-valid-in-resource-link, + consider-merging-classes-inherited, + context-overridden, + create-user-wo-reset-password, + dangerous-filter-wo-user, + dangerous-qweb-replace-wo-priority, + deprecated-data-xml-node, + deprecated-openerp-xml-node, + duplicate-po-message-definition, + except-pass, + file-not-used, + invalid-commit, + manifest-maintainers-list, + missing-newline-extrafiles, + missing-readme, + missing-return, + odoo-addons-relative-import, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + renamed-field-parameter, + resource-not-exist, + str-format-used, + test-folder-imported, + translation-contains-variable, + translation-positional-used, + unnecessary-utf8-coding-comment, + website-manifest-key-not-valid-uri, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute, + # messages that do not cause the lint step to fail + consider-merging-classes-inherited, + create-user-wo-reset-password, + dangerous-filter-wo-user, + deprecated-module, + file-not-used, + invalid-commit, + missing-manifest-dependency, + missing-newline-extrafiles, + missing-readme, + no-utf8-coding-comment, + odoo-addons-relative-import, + old-api7-method-defined, + redefined-builtin, + too-complex, + unnecessary-utf8-coding-comment + + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.pylintrc-mandatory b/.pylintrc-mandatory new file mode 100644 index 00000000..8a9afe17 --- /dev/null +++ b/.pylintrc-mandatory @@ -0,0 +1,97 @@ + +[MASTER] +load-plugins=pylint_odoo +score=n + +[ODOOLINT] +readme_template_url="https://github.com/OCA/maintainer-tools/blob/master/template/module/README.rst" +manifest_required_authors=Camptocamp +manifest_required_keys=license +manifest_deprecated_keys=description,active +license_allowed=AGPL-3,GPL-2,GPL-2 or any later version,GPL-3,GPL-3 or any later version,LGPL-3 +valid_odoo_versions=15.0 + +[MESSAGES CONTROL] +disable=all + +enable=anomalous-backslash-in-string, + api-one-deprecated, + api-one-multi-together, + assignment-from-none, + attribute-deprecated, + class-camelcase, + dangerous-default-value, + dangerous-view-replace-wo-priority, + development-status-allowed, + duplicate-id-csv, + duplicate-key, + duplicate-xml-fields, + duplicate-xml-record-id, + eval-referenced, + eval-used, + incoherent-interpreter-exec-perm, + license-allowed, + manifest-author-string, + manifest-deprecated-key, + manifest-required-author, + manifest-required-key, + manifest-version-format, + method-compute, + method-inverse, + method-required-super, + method-search, + openerp-exception-warning, + pointless-statement, + pointless-string-statement, + print-used, + redundant-keyword-arg, + redundant-modulename-xml, + reimported, + relative-import, + return-in-init, + rst-syntax-error, + sql-injection, + too-few-format-args, + translation-field, + translation-required, + unreachable, + use-vim-comment, + wrong-tabs-instead-of-spaces, + xml-syntax-error, + attribute-string-redundant, + character-not-valid-in-resource-link, + consider-merging-classes-inherited, + context-overridden, + create-user-wo-reset-password, + dangerous-filter-wo-user, + dangerous-qweb-replace-wo-priority, + deprecated-data-xml-node, + deprecated-openerp-xml-node, + duplicate-po-message-definition, + except-pass, + file-not-used, + invalid-commit, + manifest-maintainers-list, + missing-newline-extrafiles, + missing-readme, + missing-return, + odoo-addons-relative-import, + old-api7-method-defined, + po-msgstr-variables, + po-syntax-error, + renamed-field-parameter, + resource-not-exist, + str-format-used, + test-folder-imported, + translation-contains-variable, + translation-positional-used, + unnecessary-utf8-coding-comment, + website-manifest-key-not-valid-uri, + xml-attribute-translatable, + xml-deprecated-qweb-directive, + xml-deprecated-tree-attribute + +[REPORTS] +msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} +output-format=colorized +reports=no diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ce0ca1ca..00000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -language: python -sudo: false -cache: pip - -branches: - only: - - "/^[[:digit:]]{1,2}.[[:digit:]]$/" - -python: - # Force a newer version than 3.7.1 which break build - # due to https://bugs.python.org/issue34921 - - "3.7.2" - -addons: - postgresql: "9.5" - apt: - packages: - - expect-dev # provides unbuffer utility - - python-lxml # because pip installation is slow - - python-simplejson - - python-serial - -env: - matrix: - - LINT_CHECK="1" - - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_exoscale" - - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_exoscale" - - TESTS="1" ODOO_REPO="odoo/odoo" INCLUDE="cloud_platform_ovh" - - TESTS="1" ODOO_REPO="OCA/OCB" INCLUDE="cloud_platform_ovh" - - TESTS="1" ODOO_REPO="odoo/odoo" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_exoscale" - - TESTS="1" ODOO_REPO="OCA/OCB" EXCLUDE="cloud_platform,cloud_platform_ovh,cloud_platform_exoscale" - global: - - VERSION="15.0" LINT_CHECK="0" TESTS="0" - -install: - - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - - travis_install_nightly - -script: - - travis_run_tests - -after_success: - - travis_after_test_success diff --git a/LICENSE b/LICENSE index 3ffc5678..be3f7b28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ -GNU AFFERO GENERAL PUBLIC LICENSE + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -633,8 +633,8 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, @@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -658,4 +658,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. \ No newline at end of file +. diff --git a/README.md b/README.md index a3e64903..d2f1c61d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -[![Build Status](https://travis-ci.com/camptocamp/odoo-cloud-platform.svg?token=Lpp9PcS5on9AGbp76WKB&branch=12.0)](https://travis-ci.com/camptocamp/odoo-cloud-platform) + +[![Pre-commit Status](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml/badge.svg?branch=15.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml?query=branch%3A15.0) +[![Build Status](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml/badge.svg?branch=15.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml?query=branch%3A15.0) +[![codecov](https://codecov.io/gh/camptocamp/odoo-cloud-platform/branch/15.0/graph/badge.svg)](https://codecov.io/gh/camptocamp/odoo-cloud-platform) + + + # Odoo Cloud Addons @@ -167,3 +173,26 @@ The checks can be bypassed with the environment variable To prevent object storage to be accessed while failing for any kind of reason set this environment variable `DISABLE_ATTACHMENT_STORAGE` set to `1`. + + + + + +[//]: # (addons) + +This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools. + +[//]: # (end addons) + + + +## Licenses + +This repository is licensed under [AGPL-3.0](LICENSE). + +However, each module can have a totally different license, as long as they adhere to Camptocamp +policy. Consult each module's `__manifest__.py` file, which contains a `license` key +that explains its license. + +---- + diff --git a/attachment_azure/models/ir_attachment.py b/attachment_azure/models/ir_attachment.py index a9e2113a..fd15a21e 100644 --- a/attachment_azure/models/ir_attachment.py +++ b/attachment_azure/models/ir_attachment.py @@ -12,13 +12,13 @@ _logger = logging.getLogger(__name__) try: + from azure.core.exceptions import HttpResponseError, ResourceExistsError from azure.storage.blob import ( + AccountSasPermissions, BlobServiceClient, - generate_account_sas, ResourceTypes, - AccountSasPermissions, + generate_account_sas, ) - from azure.core.exceptions import ResourceExistsError, HttpResponseError except ImportError: _logger.debug("Cannot 'import azure-storage-blob'.") @@ -32,9 +32,7 @@ class IrAttachment(models.Model): _inherit = "ir.attachment" def _get_stores(self): - l = ["azure"] - l += super(IrAttachment, self)._get_stores() - return l + return ["azure"] + super(IrAttachment, self)._get_stores() @api.model def _get_blob_service_client(self): @@ -88,7 +86,7 @@ def _get_blob_service_client(self): "Error during the connection to Azure container using the " "connection string." ) - raise exceptions.UserError(str(error)) + raise exceptions.UserError(str(error)) from None else: try: sas_token = generate_account_sas( @@ -107,21 +105,16 @@ def _get_blob_service_client(self): "Error during the connection to Azure container using the Shared " "Access Signature (SAS)" ) - raise exceptions.UserError(str(error)) + raise exceptions.UserError(str(error)) from None return blob_service_client @api.model def _get_container_name(self): - """ - Container naming rules: - https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names - """ + # Container naming rules: + # https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names # noqa: B950 running_env = os.environ.get("RUNNING_ENV", "dev") - storage_name = os.environ.get('AZURE_STORAGE_NAME', r'{env}-{db}') - storage_name = storage_name.format( - env=running_env, - db=self.env.cr.dbname - ) + storage_name = os.environ.get("AZURE_STORAGE_NAME", r"{env}-{db}") + storage_name = storage_name.format(env=running_env, db=self.env.cr.dbname) # replace invalid characters by _ storage_name = re.sub(r"[\W_]+", "-", storage_name) # lowercase, max 63 chars @@ -136,7 +129,7 @@ def _get_azure_container(self, container_name=None): except exceptions.UserError: _logger.exception( "error accessing to storage '%s' please check credentials ", - container_name + container_name, ) return False container_client = blob_service_client.get_container_client(container_name) @@ -146,21 +139,21 @@ def _get_azure_container(self, container_name=None): container_client.create_container() except HttpResponseError as error: _logger.exception("Error during the creation of the Azure container") - raise exceptions.UserError(str(error)) + raise exceptions.UserError(str(error)) from None return container_client @api.model def _store_file_read(self, fname, bin_size=False): if fname.startswith("azure://"): key = fname.replace("azure://", "", 1).lower() - if '/' in key: - container_name, key = key.split('/', 1) + if "/" in key: + container_name, key = key.split("/", 1) else: container_name = None container_client = self._get_azure_container(container_name) # if container cannot be retrived, abort reading from azure storage if not container_client: - return '' + return "" try: blob_client = container_client.get_blob_client(key) read = blob_client.download_blob().readall() @@ -184,13 +177,17 @@ def _store_file_write(self, key, bin_data): try: blob_client.upload_blob(file, blob_type="BlockBlob") except ResourceExistsError: - pass + _logger.exception( + "Trying to re create an existing resource %s" % filename + ) except HttpResponseError as error: # log verbose error from azure, return short message for user - _logger.exception("Error during storage of the file %s" % filename) + _logger.exception( + "HTTP Error during storage of the file %s" % filename + ) raise exceptions.UserError( _("The file could not be stored: %s") % str(error) - ) + ) from None else: _super = super(IrAttachment, self) filename = _super._store_file_write(key, bin_data) @@ -200,13 +197,13 @@ def _store_file_write(self, key, bin_data): def _store_file_delete(self, fname): if fname.startswith("azure://"): key = fname.replace("azure://", "", 1).lower() - if '/' in key: - container_name, key = key.split('/', 1) + if "/" in key: + container_name, key = key.split("/", 1) else: container_name = None container_client = self._get_azure_container(container_name) if not container_client: - return '' + return "" # delete the file only if it is on the current configured container # otherwise, we might delete files used on a different environment try: diff --git a/attachment_s3/__init__.py b/attachment_s3/__init__.py index a9e33722..0650744f 100644 --- a/attachment_s3/__init__.py +++ b/attachment_s3/__init__.py @@ -1,2 +1 @@ - from . import models diff --git a/attachment_s3/__manifest__.py b/attachment_s3/__manifest__.py index a8fa7710..37850b16 100644 --- a/attachment_s3/__manifest__.py +++ b/attachment_s3/__manifest__.py @@ -2,17 +2,18 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Attachments on S3 storage", - "summary": "Store assets and attachments on a S3 compatible object storage", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "Knowledge Management", - "depends": ["base", "base_attachment_object_storage"], - "external_dependencies": { - "python": ["boto3"], - }, - "website": "https://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Attachments on S3 storage", + "summary": "Store assets and attachments on a S3 compatible object storage", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Knowledge Management", + "depends": ["base", "base_attachment_object_storage"], + "external_dependencies": { + "python": ["boto3"], + }, + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/attachment_s3/models/__init__.py b/attachment_s3/models/__init__.py index 3cf36e5c..aaf38a16 100644 --- a/attachment_s3/models/__init__.py +++ b/attachment_s3/models/__init__.py @@ -1,2 +1 @@ - from . import ir_attachment diff --git a/attachment_s3/models/ir_attachment.py b/attachment_s3/models/ir_attachment.py index fcedb8c0..d18c0e23 100644 --- a/attachment_s3/models/ir_attachment.py +++ b/attachment_s3/models/ir_attachment.py @@ -2,12 +2,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import io import logging import os -import io from urllib.parse import urlsplit from odoo import _, api, exceptions, models + from ..s3uri import S3Uri _logger = logging.getLogger(__name__) @@ -26,9 +27,7 @@ class IrAttachment(models.Model): _inherit = "ir.attachment" def _get_stores(self): - l = ['s3'] - l += super()._get_stores() - return l + return ["s3"] + super()._get_stores() @api.model def _get_s3_bucket(self, name=None): @@ -45,42 +44,43 @@ def _get_s3_bucket(self, name=None): from the environment variable ``AWS_BUCKETNAME`` will be read. """ - host = os.environ.get('AWS_HOST') + host = os.environ.get("AWS_HOST") # Ensure host is prefixed with a scheme (use https as default) if host and not urlsplit(host).scheme: - host = 'https://%s' % host + host = "https://%s" % host - region_name = os.environ.get('AWS_REGION') - access_key = os.environ.get('AWS_ACCESS_KEY_ID') - secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY') - bucket_name = name or os.environ.get('AWS_BUCKETNAME') + region_name = os.environ.get("AWS_REGION") + access_key = os.environ.get("AWS_ACCESS_KEY_ID") + secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY") + bucket_name = name or os.environ.get("AWS_BUCKETNAME") # replaces {db} by the database name to handle multi-tenancy bucket_name = bucket_name.format(db=self.env.cr.dbname) params = { - 'aws_access_key_id': access_key, - 'aws_secret_access_key': secret_key, + "aws_access_key_id": access_key, + "aws_secret_access_key": secret_key, } if host: - params['endpoint_url'] = host + params["endpoint_url"] = host if region_name: - params['region_name'] = region_name + params["region_name"] = region_name if not (access_key and secret_key and bucket_name): - msg = _('If you want to read from the %s S3 bucket, the following ' - 'environment variables must be set:\n' - '* AWS_ACCESS_KEY_ID\n' - '* AWS_SECRET_ACCESS_KEY\n' - 'If you want to write in the %s S3 bucket, this variable ' - 'must be set as well:\n' - '* AWS_BUCKETNAME\n' - 'Optionally, the S3 host can be changed with:\n' - '* AWS_HOST\n' - ) % (bucket_name, bucket_name) + msg = _( + "If you want to read from the %(bucket_name)s S3 bucket, the following " + "environment variables must be set:\n" + "* AWS_ACCESS_KEY_ID\n" + "* AWS_SECRET_ACCESS_KEY\n" + "If you want to write in the %(bucket_name)s S3 bucket, this variable " + "must be set as well:\n" + "* AWS_BUCKETNAME\n" + "Optionally, the S3 host can be changed with:\n" + "* AWS_HOST\n" + ).format(bucket_name=bucket_name) raise exceptions.UserError(msg) # try: - s3 = boto3.resource('s3', **params) + s3 = boto3.resource("s3", **params) bucket = s3.Bucket(bucket_name) exists = True try: @@ -88,13 +88,13 @@ def _get_s3_bucket(self, name=None): except ClientError as e: # If a client error is thrown, then check that it was a 404 error. # If it was a 404 error, then the bucket does not exist. - error_code = e.response['Error']['Code'] - if error_code == '404': + error_code = e.response["Error"]["Code"] + if error_code == "404": exists = False except EndpointConnectionError as error: # log verbose error from s3, return short message for user - _logger.exception('Error during connection on S3') - raise exceptions.UserError(str(error)) + msg = _logger.exception("Error during connection on S3") + raise exceptions.UserError(str(error)) from None if not exists: if not region_name: @@ -102,14 +102,13 @@ def _get_s3_bucket(self, name=None): else: bucket = s3.create_bucket( Bucket=bucket_name, - CreateBucketConfiguration={ - 'LocationConstraint': region_name - }) + CreateBucketConfiguration={"LocationConstraint": region_name}, + ) return bucket @api.model def _store_file_read(self, fname): - if fname.startswith('s3://'): + if fname.startswith("s3://"): s3uri = S3Uri(fname) try: bucket = self._get_s3_bucket(name=s3uri.bucket()) @@ -117,45 +116,39 @@ def _store_file_read(self, fname): _logger.exception( "error reading attachment '%s' from object storage", fname ) - return '' + return "" try: key = s3uri.item() - bucket.meta.client.head_object( - Bucket=bucket.name, Key=key - ) + bucket.meta.client.head_object(Bucket=bucket.name, Key=key) with io.BytesIO() as res: bucket.download_fileobj(key, res) res.seek(0) read = res.read() except ClientError: - read = '' - _logger.info( - "attachment '%s' missing on object storage", fname - ) + read = "" + _logger.info("attachment '%s' missing on object storage", fname) return read else: return super()._store_file_read(fname) @api.model def _store_file_write(self, key, bin_data): - location = self.env.context.get('storage_location') or self._storage() - if location == 's3': + location = self.env.context.get("storage_location") or self._storage() + if location == "s3": bucket = self._get_s3_bucket() obj = bucket.Object(key=key) with io.BytesIO() as file: file.write(bin_data) file.seek(0) - filename = 's3://%s/%s' % (bucket.name, key) + filename = "s3://%s/%s" % (bucket.name, key) try: obj.upload_fileobj(file) except ClientError as error: # log verbose error from s3, return short message for user - _logger.exception( - 'Error during storage of the file %s' % filename - ) + _logger.exception("Error during storage of the file %s" % filename) raise exceptions.UserError( - _('The file could not be stored: %s') % str(error) - ) + _("The file could not be stored: %s") % str(error) + ) from None else: _super = super() filename = _super._store_file_write(key, bin_data) @@ -163,28 +156,22 @@ def _store_file_write(self, key, bin_data): @api.model def _store_file_delete(self, fname): - if fname.startswith('s3://'): + if fname.startswith("s3://"): s3uri = S3Uri(fname) bucket_name = s3uri.bucket() item_name = s3uri.item() # delete the file only if it is on the current configured bucket # otherwise, we might delete files used on a different environment - if bucket_name == os.environ.get('AWS_BUCKETNAME'): + if bucket_name == os.environ.get("AWS_BUCKETNAME"): bucket = self._get_s3_bucket() obj = bucket.Object(key=item_name) try: - bucket.meta.client.head_object( - Bucket=bucket.name, Key=item_name - ) + bucket.meta.client.head_object(Bucket=bucket.name, Key=item_name) obj.delete() - _logger.info( - 'file %s deleted on the object storage' % (fname,) - ) + _logger.info("file %s deleted on the object storage" % (fname,)) except ClientError: # log verbose error from s3, return short message for # user - _logger.exception( - 'Error during deletion of the file %s' % fname - ) + _logger.exception("Error during deletion of the file %s" % fname) else: - super()._store_file_delete(fname) + return super()._store_file_delete(fname) diff --git a/attachment_swift/__manifest__.py b/attachment_swift/__manifest__.py index ac598edc..66b73b14 100644 --- a/attachment_swift/__manifest__.py +++ b/attachment_swift/__manifest__.py @@ -2,20 +2,22 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Attachments on Swift storage", - "summary": "Store assets and attachments on a Swift compatible object store", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "Knowledge Management", - "depends": ["base_attachment_object_storage"], - "external_dependencies": { - "python": ["swiftclient", - "keystoneclient", - "keystoneauth1", - ], - }, - "website": "https://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Attachments on Swift storage", + "summary": "Store assets and attachments on a Swift compatible object store", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Knowledge Management", + "depends": ["base_attachment_object_storage"], + "external_dependencies": { + "python": [ + "swiftclient", + "keystoneclient", + "keystoneauth1", + ], + }, + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/attachment_swift/models/ir_attachment.py b/attachment_swift/models/ir_attachment.py index 7fd659e1..6a8b088a 100644 --- a/attachment_swift/models/ir_attachment.py +++ b/attachment_swift/models/ir_attachment.py @@ -4,17 +4,18 @@ import logging import os -from ..swift_uri import SwiftUri -from odoo import api, exceptions, models, _ +from odoo import _, api, exceptions, models + +from ..swift_uri import SwiftUri _logger = logging.getLogger(__name__) try: - import swiftclient import keystoneauth1 import keystoneauth1.identity import keystoneauth1.session + import swiftclient from swiftclient.exceptions import ClientException except ImportError: swiftclient = None @@ -48,8 +49,9 @@ def __init__(self): def _get_key(self, auth_url, username, password, project_name): return (auth_url, username, password, project_name) - def get_session(self, auth_url=None, username=None, password=None, - project_name=None): + def get_session( + self, auth_url=None, username=None, password=None, project_name=None + ): key = self._get_key(auth_url, username, password, project_name) session = self._sessions.get(key) if not session: @@ -58,8 +60,8 @@ def get_session(self, auth_url=None, username=None, password=None, password=password, project_name=project_name, auth_url=auth_url, - project_domain_id='default', - user_domain_id='default', + project_domain_id="default", + user_domain_id="default", ) session = keystoneauth1.session.Session( auth=auth, @@ -73,36 +75,36 @@ def get_session(self, auth_url=None, username=None, password=None, class IrAttachment(models.Model): - _inherit = 'ir.attachment' + _inherit = "ir.attachment" def _get_stores(self): - l = ['swift'] - l += super()._get_stores() - return l + return ["swift"] + super()._get_stores() @api.model def _get_swift_connection(self): - """ Returns a connection object for the Swift object store """ - host = os.environ.get('SWIFT_AUTH_URL') - account = os.environ.get('SWIFT_ACCOUNT') - password = os.environ.get('SWIFT_PASSWORD') - project_name = os.environ.get('SWIFT_PROJECT_NAME') - if not project_name and os.environ.get('SWIFT_TENANT_NAME'): - project_name = os.environ['SWIFT_TENANT_NAME'] + """Returns a connection object for the Swift object store""" + host = os.environ.get("SWIFT_AUTH_URL") + account = os.environ.get("SWIFT_ACCOUNT") + password = os.environ.get("SWIFT_PASSWORD") + project_name = os.environ.get("SWIFT_PROJECT_NAME") + if not project_name and os.environ.get("SWIFT_TENANT_NAME"): + project_name = os.environ["SWIFT_TENANT_NAME"] _logger.warning( "SWIFT_TENANT_NAME is deprecated and " "must be replaced by SWIFT_PROJECT_NAME" ) - region = os.environ.get('SWIFT_REGION_NAME') + region = os.environ.get("SWIFT_REGION_NAME") os_options = {} if region: - os_options['region_name'] = region + os_options["region_name"] = region if not (host and account and password and project_name): - raise exceptions.UserError(_( - "Problem connecting to Swift store, are the env variables " - "(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, " - "SWIFT_TENANT_NAME) properly set?" - )) + raise exceptions.UserError( + _( + "Problem connecting to Swift store, are the env variables " + "(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, " + "SWIFT_TENANT_NAME) properly set?" + ) + ) try: session = swift_session_store.get_session( username=account, @@ -115,13 +117,13 @@ def _get_swift_connection(self): os_options=os_options, ) except ClientException: - _logger.exception('Error connecting to Swift object store') - raise exceptions.UserError(_('Error on Swift connection')) + _logger.exception("Error connecting to Swift object store") + raise exceptions.UserError(_("Error on Swift connection")) from None return conn @api.model def _store_file_read(self, fname): - if fname.startswith('swift://'): + if fname.startswith("swift://"): swifturi = SwiftUri(fname) try: conn = self._get_swift_connection() @@ -129,31 +131,27 @@ def _store_file_read(self, fname): _logger.exception( "error reading attachment '%s' from object storage", fname ) - return '' + return "" try: - resp, read = conn.get_object( - swifturi.container(), - swifturi.item() - ) + resp, read = conn.get_object(swifturi.container(), swifturi.item()) except ClientException: - read = '' - _logger.exception( - 'Error reading object from Swift object store') + read = "" + _logger.exception("Error reading object from Swift object store") return read else: return super()._store_file_read(fname) def _store_file_write(self, key, bin_data): - if self._storage() == 'swift': - container = os.environ.get('SWIFT_WRITE_CONTAINER') + if self._storage() == "swift": + container = os.environ.get("SWIFT_WRITE_CONTAINER") conn = self._get_swift_connection() conn.put_container(container) - filename = 'swift://{}/{}'.format(container, key) + filename = "swift://{}/{}".format(container, key) try: conn.put_object(container, key, bin_data) except ClientException: - _logger.exception('Error writing to Swift object store') - raise exceptions.UserError(_('Error writing to Swift')) + _logger.exception("Error writing to Swift object store") + raise exceptions.UserError(_("Error writing to Swift")) from None else: _super = super() filename = _super._store_file_write(key, bin_data) @@ -161,19 +159,18 @@ def _store_file_write(self, key, bin_data): @api.model def _store_file_delete(self, fname): - if fname.startswith('swift://'): + if fname.startswith("swift://"): swifturi = SwiftUri(fname) container = swifturi.container() # delete the file only if it is on the current configured bucket # otherwise, we might delete files used on a different environment - if container == os.environ.get('SWIFT_WRITE_CONTAINER'): + if container == os.environ.get("SWIFT_WRITE_CONTAINER"): conn = self._get_swift_connection() try: conn.delete_object(container, swifturi.item()) except ClientException: - _logger.exception( - _('Error deleting an object on the Swift store')) + _logger.exception(_("Error deleting an object on the Swift store")) # we ignore the error, file will stay on the object # storage but won't disrupt the process else: - super()._file_delete_from_store(fname) + return super()._file_delete_from_store(fname) diff --git a/attachment_swift/swift_uri.py b/attachment_swift/swift_uri.py index fdd7ef48..f12c841f 100644 --- a/attachment_swift/swift_uri.py +++ b/attachment_swift/swift_uri.py @@ -6,8 +6,7 @@ class SwiftUri(object): - _url_re = re.compile("^swift:///*([^/]*)/?(.*)", - re.IGNORECASE | re.UNICODE) + _url_re = re.compile("^swift:///*([^/]*)/?(.*)", re.IGNORECASE | re.UNICODE) def __init__(self, uri): match = self._url_re.match(uri) diff --git a/attachment_swift/tests/__init__.py b/attachment_swift/tests/__init__.py index 506e6b2d..8a60b377 100644 --- a/attachment_swift/tests/__init__.py +++ b/attachment_swift/tests/__init__.py @@ -1,2 +1 @@ - from . import test_mock_swift_api diff --git a/attachment_swift/tests/test_mock_swift_api.py b/attachment_swift/tests/test_mock_swift_api.py index 11a54212..a13a80f5 100644 --- a/attachment_swift/tests/test_mock_swift_api.py +++ b/attachment_swift/tests/test_mock_swift_api.py @@ -2,30 +2,28 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import base64 -import mock import os -from mock import patch - import keystoneauth1 +import mock +from mock import patch -from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment from odoo.addons.attachment_swift.models.ir_attachment import SwiftSessionStore from odoo.addons.attachment_swift.swift_uri import SwiftUri +from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment class TestAttachmentSwift(TestIrAttachment): - def setup(self): - super().setUp() - self.env['ir.config_parameter'].set_param('ir_attachment.location', - 'swift') + res = super().setUp() + self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift") + return res def test_session_store_get_session(self): - auth_url = 'auth_url' - username = 'username' - password = 'password' - project_name = 'project_name' + auth_url = "auth_url" + username = "username" + password = "password" + project_name = "project_name" store = SwiftSessionStore() session = store.get_session( auth_url=auth_url, @@ -34,10 +32,12 @@ def test_session_store_get_session(self): project_name=project_name, ) self.assertEqual(session.auth.auth_url, auth_url) - self.assertEqual(session.auth.get_cache_id_elements().get( - 'password_username'), username) - self.assertEqual(session.auth.get_cache_id_elements().get( - 'password_password'), password) + self.assertEqual( + session.auth.get_cache_id_elements().get("password_username"), username + ) + self.assertEqual( + session.auth.get_cache_id_elements().get("password_password"), password + ) self.assertEqual(session.auth.project_name, project_name) # get the same session on a second call @@ -48,73 +48,73 @@ def test_session_store_get_session(self): password=password, project_name=project_name, ), - session + session, ) - @patch('swiftclient.client') + @patch("swiftclient.client") def test_connection(self, mock_swift_client): - """ Test the connection to the store""" - os.environ['SWIFT_AUTH_URL'] = 'auth_url' - os.environ['SWIFT_ACCOUNT'] = 'account' - os.environ['SWIFT_PASSWORD'] = 'password' - os.environ['SWIFT_PROJECT_NAME'] = 'project_name' - os.environ['SWIFT_REGION_NAME'] = 'NOWHERE' + """Test the connection to the store""" + os.environ["SWIFT_AUTH_URL"] = "auth_url" + os.environ["SWIFT_ACCOUNT"] = "account" + os.environ["SWIFT_PASSWORD"] = "password" + os.environ["SWIFT_PROJECT_NAME"] = "project_name" + os.environ["SWIFT_REGION_NAME"] = "NOWHERE" attachment = self.Attachment attachment._get_swift_connection() mock_swift_client.Connection.assert_called_once_with( session=mock.ANY, - os_options={'region_name': os.environ.get('SWIFT_REGION_NAME')}, + os_options={"region_name": os.environ.get("SWIFT_REGION_NAME")}, ) __, kwargs = mock_swift_client.Connection.call_args - session = kwargs['session'] + session = kwargs["session"] self.assertTrue(isinstance(session, keystoneauth1.session.Session)) - self.assertEqual(session.auth.auth_url, os.environ['SWIFT_AUTH_URL']) - self.assertEqual(session.auth.get_cache_id_elements().get( - 'password_username'), os.environ['SWIFT_ACCOUNT']) - self.assertEqual(session.auth.get_cache_id_elements().get( - 'password_password'), os.environ['SWIFT_PASSWORD']) - self.assertEqual(session.auth.project_name, - os.environ['SWIFT_PROJECT_NAME']) + self.assertEqual(session.auth.auth_url, os.environ["SWIFT_AUTH_URL"]) + self.assertEqual( + session.auth.get_cache_id_elements().get("password_username"), + os.environ["SWIFT_ACCOUNT"], + ) + self.assertEqual( + session.auth.get_cache_id_elements().get("password_password"), + os.environ["SWIFT_PASSWORD"], + ) + self.assertEqual(session.auth.project_name, os.environ["SWIFT_PROJECT_NAME"]) def test_store_file_on_swift(self): """ - Test writing a file + Test writing a file """ - (self.env['ir.config_parameter']. - set_param('ir_attachment.location', 'swift')) - os.environ['SWIFT_AUTH_URL'] = 'auth_url' - os.environ['SWIFT_ACCOUNT'] = 'account' - os.environ['SWIFT_PASSWORD'] = 'password' - os.environ['SWIFT_PROJECT_NAME'] = 'project_name' - os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container' - container = os.environ.get('SWIFT_WRITE_CONTAINER') + (self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")) + os.environ["SWIFT_AUTH_URL"] = "auth_url" + os.environ["SWIFT_ACCOUNT"] = "account" + os.environ["SWIFT_PASSWORD"] = "password" + os.environ["SWIFT_PROJECT_NAME"] = "project_name" + os.environ["SWIFT_WRITE_CONTAINER"] = "my_container" + container = os.environ.get("SWIFT_WRITE_CONTAINER") attachment = self.Attachment bin_data = base64.b64decode(self.blob1_b64) - with patch('swiftclient.client.Connection') as MockConnection: + with patch("swiftclient.client.Connection") as MockConnection: conn = MockConnection.return_value - attachment.create({'name': 'a5', 'datas': self.blob1_b64}) + attachment.create({"name": "a5", "datas": self.blob1_b64}) conn.put_object.assert_called_with( - container, - attachment._compute_checksum(bin_data), - bin_data) + container, attachment._compute_checksum(bin_data), bin_data + ) def test_delete_file_on_swift(self): """ - Test deleting a file + Test deleting a file """ - (self.env['ir.config_parameter']. - set_param('ir_attachment.location', 'swift')) - os.environ['SWIFT_AUTH_URL'] = 'auth_url' - os.environ['SWIFT_ACCOUNT'] = 'account' - os.environ['SWIFT_PASSWORD'] = 'password' - os.environ['SWIFT_PROJECT_NAME'] = 'project_name' - os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container' + (self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")) + os.environ["SWIFT_AUTH_URL"] = "auth_url" + os.environ["SWIFT_ACCOUNT"] = "account" + os.environ["SWIFT_PASSWORD"] = "password" + os.environ["SWIFT_PROJECT_NAME"] = "project_name" + os.environ["SWIFT_WRITE_CONTAINER"] = "my_container" attachment = self.Attachment - container = os.environ.get('SWIFT_WRITE_CONTAINER') - with patch('swiftclient.client.Connection') as MockConnection: + container = os.environ.get("SWIFT_WRITE_CONTAINER") + with patch("swiftclient.client.Connection") as MockConnection: conn = MockConnection.return_value - a5 = attachment.create({'name': 'a5', 'datas': self.blob1_b64}) + a5 = attachment.create({"name": "a5", "datas": self.blob1_b64}) uri = SwiftUri(a5.store_fname) a5.unlink() conn.delete_object.assert_called_with(container, uri.item()) diff --git a/attachment_swift/tests/test_with_swift_store.py b/attachment_swift/tests/test_with_swift_store.py index e46ba41f..83823b6b 100644 --- a/attachment_swift/tests/test_with_swift_store.py +++ b/attachment_swift/tests/test_with_swift_store.py @@ -1,9 +1,11 @@ # Copyright 2017-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from swiftclient.exceptions import ClientException + from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment + from ..swift_uri import SwiftUri -from swiftclient.exceptions import ClientException class TestAttachmentSwift(TestIrAttachment): @@ -12,28 +14,26 @@ class TestAttachmentSwift(TestIrAttachment): """ def setup(self): - super().setUp() - self.env['ir.config_parameter'].set_param('ir_attachment.location', - 'swift') + res = super().setUp() + self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift") + return res def test_connection(self): - """ Test the connection to the Swift object store """ + """Test the connection to the Swift object store""" conn = self.Attachment._get_swift_connection() self.assertNotEqual(conn, False) def test_store_file_on_swift(self): - """ Test writing a file and then reading it """ - (self.env['ir.config_parameter']. - set_param('ir_attachment.location', 'swift')) - a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64}) + """Test writing a file and then reading it""" + (self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")) + a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64}) a5bis = self.Attachment.browse(a5.id)[0] self.assertEqual(a5.datas, a5bis.datas) def test_delete_file_on_swift(self): - """ Create a file and then test the deletion """ - (self.env['ir.config_parameter']. - set_param('ir_attachment.location', 'swift')) - a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64}) + """Create a file and then test the deletion""" + (self.env["ir.config_parameter"].set_param("ir_attachment.location", "swift")) + a5 = self.Attachment.create({"name": "a5", "datas": self.blob1_b64}) uri = SwiftUri(a5.store_fname) con = self.Attachment._get_swift_connection() con.get_object(uri.container(), uri.item()) diff --git a/base_attachment_object_storage/__manifest__.py b/base_attachment_object_storage/__manifest__.py index 9605e9ba..1d50cb69 100644 --- a/base_attachment_object_storage/__manifest__.py +++ b/base_attachment_object_storage/__manifest__.py @@ -2,15 +2,16 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Base Attachment Object Store", - "summary": "Base module for the implementation of external object store.", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "Knowledge Management", - "depends": ["base"], - "website": "http://www.camptocamp.com", - "data": ["data/res_config_settings_data.xml"], - "installable": True, - "auto_install": True, - } +{ + "name": "Base Attachment Object Store", + "summary": "Base module for the implementation of external object store.", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Knowledge Management", + "depends": ["base"], + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": ["data/res_config_settings_data.xml"], + "installable": True, + "auto_install": True, +} diff --git a/base_attachment_object_storage/data/res_config_settings_data.xml b/base_attachment_object_storage/data/res_config_settings_data.xml index 76c6961d..4a1b8d4b 100644 --- a/base_attachment_object_storage/data/res_config_settings_data.xml +++ b/base_attachment_object_storage/data/res_config_settings_data.xml @@ -1,9 +1,11 @@ - + ir_attachment.storage.force.database - {"image/": 51200, "application/javascript": 0, "text/css": 0} + {"image/": 51200, "application/javascript": 0, "text/css": 0} diff --git a/base_attachment_object_storage/models/ir_attachment.py b/base_attachment_object_storage/models/ir_attachment.py index ed43c697..b7ea9f3f 100644 --- a/base_attachment_object_storage/models/ir_attachment.py +++ b/base_attachment_object_storage/models/ir_attachment.py @@ -5,53 +5,50 @@ import logging import os import time +from contextlib import closing, contextmanager from distutils.util import strtobool import psycopg2 -import odoo -from contextlib import closing, contextmanager -from odoo import api, exceptions, models, _ +import odoo +from odoo import _, api, exceptions, models from odoo.osv.expression import AND, OR, normalize_domain from odoo.tools.safe_eval import const_eval - _logger = logging.getLogger(__name__) def is_true(strval): - return bool(strtobool(strval or '0')) + return bool(strtobool(strval or "0")) def clean_fs(files): - _logger.info('cleaning old files from filestore') + _logger.info("cleaning old files from filestore") for full_path in files: if os.path.exists(full_path): try: os.unlink(full_path) except OSError: _logger.info( - "_file_delete could not unlink %s", - full_path, exc_info=True + "_file_delete could not unlink %s", full_path, exc_info=True ) except IOError: # Harmless and needed for race conditions _logger.info( - "_file_delete could not unlink %s", - full_path, exc_info=True + "_file_delete could not unlink %s", full_path, exc_info=True ) class IrAttachment(models.Model): - _inherit = 'ir.attachment' + _inherit = "ir.attachment" @staticmethod def is_storage_disabled(storage=None, log=True): msg = _("Storages are disabled (see environment configuration).") if storage: - msg = _( - "Storage '%s' is disabled (see environment configuration)." - ) % (storage,) + msg = _("Storage '%s' is disabled (see environment configuration).") % ( + storage, + ) is_disabled = is_true(os.environ.get("DISABLE_ATTACHMENT_STORAGE")) if is_disabled and log: _logger.warning(msg) @@ -59,7 +56,7 @@ def is_storage_disabled(storage=None, log=True): def _register_hook(self): super()._register_hook() - location = self.env.context.get('storage_location') or self._storage() + location = self.env.context.get("storage_location") or self._storage() # ignore if we are not using an object storage if location not in self._get_stores(): return @@ -73,7 +70,7 @@ def _register_hook(self): # done during the initialization. We need to move the attachments that # could have been created or updated in other addons before this addon # was loaded - update_module = load_modules_frame.f_locals.get('update_module') + update_module = load_modules_frame.f_locals.get("update_module") # We need to call the migration on the loading of the model because # when we are upgrading addons, some of them might add attachments. @@ -82,15 +79,19 @@ def _register_hook(self): # Typical example is images of ir.ui.menu which are updated in # ir.attachment at every upgrade of the addons if update_module: - self.env['ir.attachment'].sudo()._force_storage_to_object_storage() + self.env["ir.attachment"].sudo()._force_storage_to_object_storage() @property def _object_storage_default_force_db_config(self): return {"image/": 51200, "application/javascript": 0, "text/css": 0} def _get_storage_force_db_config(self): - param = self.env['ir.config_parameter'].sudo().get_param( - 'ir_attachment.storage.force.database', + param = ( + self.env["ir.config_parameter"] + .sudo() + .get_param( + "ir_attachment.storage.force.database", + ) ) storage_config = None if param: @@ -100,7 +101,8 @@ def _get_storage_force_db_config(self): _logger.exception( "Could not parse system parameter" " 'ir_attachment.storage.force.database', reverting to the" - " default configuration.") + " default configuration." + ) if not storage_config: storage_config = self._object_storage_default_force_db_config @@ -128,7 +130,7 @@ def _store_in_db_instead_of_object_storage_domain(self): return domain def _store_in_db_instead_of_object_storage(self, data, mimetype): - """ Return whether an attachment must be stored in db + """Return whether an attachment must be stored in db When we are using an Object Storage. This is sometimes required because the object storage is slower than the database/filesystem. @@ -180,17 +182,17 @@ def _store_in_db_instead_of_object_storage(self, data, mimetype): return False def _get_datas_related_values(self, data, mimetype): - storage = self.env.context.get('storage_location') or self._storage() + storage = self.env.context.get("storage_location") or self._storage() if data and storage in self._get_stores(): if self._store_in_db_instead_of_object_storage(data, mimetype): # compute the fields that depend on datas bin_data = data values = { - 'file_size': len(bin_data), - 'checksum': self._compute_checksum(bin_data), - 'index_content': self._index(bin_data, mimetype), - 'store_fname': False, - 'db_datas': data, + "file_size": len(bin_data), + "checksum": self._compute_checksum(bin_data), + "index_content": self._index(bin_data, mimetype), + "store_fname": False, + "db_datas": data, } return values return super()._get_datas_related_values(data, mimetype) @@ -203,28 +205,22 @@ def _file_read(self, fname): return super()._file_read(fname) def _store_file_read(self, fname): - storage = fname.partition('://')[0] - raise NotImplementedError( - 'No implementation for %s' % (storage,) - ) + storage = fname.partition("://")[0] + raise NotImplementedError("No implementation for %s" % (storage,)) def _store_file_write(self, key, bin_data): storage = self.storage() - raise NotImplementedError( - 'No implementation for %s' % (storage,) - ) + raise NotImplementedError("No implementation for %s" % (storage,)) def _store_file_delete(self, fname): - storage = fname.partition('://')[0] - raise NotImplementedError( - 'No implementation for %s' % (storage,) - ) + storage = fname.partition("://")[0] + raise NotImplementedError("No implementation for %s" % (storage,)) @api.model def _file_write(self, bin_data, checksum): - location = self.env.context.get('storage_location') or self._storage() + location = self.env.context.get("storage_location") or self._storage() if location in self._get_stores(): - key = self.env.context.get('force_storage_key') + key = self.env.context.get("force_storage_key") if not key: key = self._compute_checksum(bin_data) filename = self._store_file_write(key, bin_data) @@ -238,35 +234,34 @@ def _file_delete(self, fname): cr = self.env.cr # using SQL to include files hidden through unlink or due to record # rules - cr.execute("SELECT COUNT(*) FROM ir_attachment " - "WHERE store_fname = %s", (fname,)) + cr.execute( + "SELECT COUNT(*) FROM ir_attachment " "WHERE store_fname = %s", (fname,) + ) count = cr.fetchone()[0] if not count: self._store_file_delete(fname) else: - super()._file_delete(fname) + return super()._file_delete(fname) @api.model def _is_file_from_a_store(self, fname): for store_name in self._get_stores(): if self.is_storage_disabled(store_name): continue - uri = '{}://'.format(store_name) + uri = "{}://".format(store_name) if fname.startswith(uri): return True return False @contextmanager def do_in_new_env(self, new_cr=False): - """ Context manager that yields a new environment + """Context manager that yields a new environment Using a new Odoo Environment thus a new PG transaction. """ with api.Environment.manage(): if new_cr: - registry = odoo.modules.registry.Registry.new( - self.env.cr.dbname - ) + registry = odoo.modules.registry.Registry.new(self.env.cr.dbname) with closing(registry.cursor()) as cr: try: yield self.env(cr=cr) @@ -283,33 +278,38 @@ def do_in_new_env(self, new_cr=False): def _move_attachment_to_store(self): self.ensure_one() - _logger.info('inspecting attachment %s (%d)', self.name, self.id) + _logger.info("inspecting attachment %s (%d)", self.name, self.id) fname = self.store_fname - storage = fname.partition('://')[0] + storage = fname.partition("://")[0] if self.is_storage_disabled(storage): fname = False if fname: # migrating from filesystem filestore # or from the old 'store_fname' without the bucket name - _logger.info('moving %s on the object storage', fname) - self.write({'datas': self.datas, - # this is required otherwise the - # mimetype gets overriden with - # 'application/octet-stream' - # on assets - 'mimetype': self.mimetype}) - _logger.info('moved %s on the object storage', fname) + _logger.info("moving %s on the object storage", fname) + self.write( + { + "datas": self.datas, + # this is required otherwise the + # mimetype gets overriden with + # 'application/octet-stream' + # on assets + "mimetype": self.mimetype, + } + ) + _logger.info("moved %s on the object storage", fname) return self._full_path(fname) elif self.db_datas: - _logger.info('moving on the object storage from database') - self.write({'datas': self.datas}) + _logger.info("moving on the object storage from database") + self.write({"datas": self.datas}) @api.model def force_storage(self): - if not self.env['res.users'].browse(self.env.uid)._is_admin(): + if not self.env["res.users"].browse(self.env.uid)._is_admin(): raise exceptions.AccessError( - _('Only administrators can execute this action.')) - location = self.env.context.get('storage_location') or self._storage() + _("Only administrators can execute this action.") + ) + location = self.env.context.get("storage_location") or self._storage() if location not in self._get_stores(): return super().force_storage() self._force_storage_to_object_storage() @@ -335,30 +335,32 @@ def force_storage_to_db_for_special_fields(self, new_cr=False): if storage not in self._get_stores(): return - domain = AND(( - normalize_domain( - [('store_fname', '=like', '{}://%'.format(storage)), - # for res_field, see comment in - # _force_storage_to_object_storage - '|', - ('res_field', '=', False), - ('res_field', '!=', False), - ] - ), - normalize_domain(self._store_in_db_instead_of_object_storage_domain()) - )) + domain = AND( + ( + normalize_domain( + [ + ("store_fname", "=like", "{}://%".format(storage)), + # for res_field, see comment in + # _force_storage_to_object_storage + "|", + ("res_field", "=", False), + ("res_field", "!=", False), + ] + ), + normalize_domain(self._store_in_db_instead_of_object_storage_domain()), + ) + ) with self.do_in_new_env(new_cr=new_cr) as new_env: - model_env = new_env['ir.attachment'].with_context( - prefetch_fields=False - ) + model_env = new_env["ir.attachment"].with_context(prefetch_fields=False) attachment_ids = model_env.search(domain).ids if not attachment_ids: return total = len(attachment_ids) start_time = time.time() - _logger.info('Moving %d attachments from %s to' - ' DB for fast access', total, storage) + _logger.info( + "Moving %d attachments from %s to" " DB for fast access", total, storage + ) current = 0 for attachment_id in attachment_ids: current += 1 @@ -370,38 +372,43 @@ def force_storage_to_db_for_special_fields(self, new_cr=False): # this write will read the datas from the Object Storage and # write them back in the DB (the logic for location to write is # in the 'datas' inverse computed field) - attachment.write({'datas': attachment.datas}) + attachment.write({"datas": attachment.datas}) # as the file will potentially be dropped on the bucket, # we should commit the changes here new_env.cr.commit() if current % 100 == 0 or total - current == 0: _logger.info( - 'attachment %s/%s after %.2fs', - current, total, - time.time() - start_time + "attachment %s/%s after %.2fs", + current, + total, + time.time() - start_time, ) @api.model def _force_storage_to_object_storage(self, new_cr=False): - _logger.info('migrating files to the object storage') - storage = self.env.context.get('storage_location') or self._storage() + _logger.info("migrating files to the object storage") + storage = self.env.context.get("storage_location") or self._storage() if self.is_storage_disabled(storage): return # The weird "res_field = False OR res_field != False" domain # is required! It's because of an override of _search in ir.attachment # which adds ('res_field', '=', False) when the domain does not # contain 'res_field'. - # https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347 - domain = ['!', ('store_fname', '=like', '{}://%'.format(storage)), - '|', - ('res_field', '=', False), - ('res_field', '!=', False)] + # https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347 # noqa: B950 + + domain = [ + "!", + ("store_fname", "=like", "{}://%".format(storage)), + "|", + ("res_field", "=", False), + ("res_field", "!=", False), + ] # We do a copy of the environment so we can workaround the cache issue # below. We do not create a new cursor by default because it causes # serialization issues due to concurrent updates on attachments during # the installation with self.do_in_new_env(new_cr=new_cr) as new_env: - model_env = new_env['ir.attachment'] + model_env = new_env["ir.attachment"] ids = model_env.search(domain).ids files_to_clean = [] for attachment_id in ids: @@ -410,12 +417,14 @@ def _force_storage_to_object_storage(self, new_cr=False): # check that no other transaction has # locked the row, don't send a file to storage # in that case - self.env.cr.execute("SELECT id " - "FROM ir_attachment " - "WHERE id = %s " - "FOR UPDATE NOWAIT", - (attachment_id,), - log_exceptions=False) + self.env.cr.execute( + "SELECT id " + "FROM ir_attachment " + "WHERE id = %s " + "FOR UPDATE NOWAIT", + (attachment_id,), + log_exceptions=False, + ) # This is a trick to avoid having the 'datas' # function fields computed for every attachment on @@ -428,8 +437,9 @@ def _force_storage_to_object_storage(self, new_cr=False): if path: files_to_clean.append(path) except psycopg2.OperationalError: - _logger.error('Could not migrate attachment %s to S3', - attachment_id) + _logger.error( + "Could not migrate attachment %s to S3", attachment_id + ) def clean(): clean_fs(files_to_clean) @@ -437,8 +447,8 @@ def clean(): # delete the files from the filesystem once we know the changes # have been committed in ir.attachment if files_to_clean: - new_env.cr.after('commit', clean) + new_env.cr.after("commit", clean) def _get_stores(self): - """ To get the list of stores activated in the system """ + """To get the list of stores activated in the system""" return [] diff --git a/base_fileurl_field/__init__.py b/base_fileurl_field/__init__.py index 08405c58..73544389 100644 --- a/base_fileurl_field/__init__.py +++ b/base_fileurl_field/__init__.py @@ -1,2 +1 @@ from . import fields - diff --git a/base_fileurl_field/__manifest__.py b/base_fileurl_field/__manifest__.py index a985c872..214a5b47 100644 --- a/base_fileurl_field/__manifest__.py +++ b/base_fileurl_field/__manifest__.py @@ -6,6 +6,7 @@ "version": "15.0.1.0.0", "category": "Technical Settings", "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "license": "AGPL-3", "depends": [ "base_attachment_object_storage", diff --git a/base_fileurl_field/fields.py b/base_fileurl_field/fields.py index 133082af..720db3ac 100644 --- a/base_fileurl_field/fields.py +++ b/base_fileurl_field/fields.py @@ -4,7 +4,6 @@ from odoo import fields - fields.Field.__doc__ += """ .. _field-fileurl: @@ -29,10 +28,10 @@ class FileURL(fields.Binary): _slots = { - 'attachment': True, # Override default with True - 'storage_location': '', # External storage activated on the system (cf base_attachment_storage) # noqa - 'storage_path': '', # Path to be used as storage key (prefix of filename) # noqa - 'filename': '', # Field to use to store the filename on ir.attachment + "attachment": True, # Override default with True + "storage_location": "", # External storage activated on the system (cf base_attachment_storage) # noqa + "storage_path": "", # Path to be used as storage key (prefix of filename) # noqa + "filename": "", # Field to use to store the filename on ir.attachment } # pylint: disable=method-required-super @@ -47,22 +46,22 @@ def create(self, record_values): if not value: continue vals = { - 'name': self.name, - 'res_model': self.model_name, - 'res_field': self.name, - 'res_id': record.id, - 'type': 'binary', - 'datas': value, + "name": self.name, + "res_model": self.model_name, + "res_field": self.name, + "res_id": record.id, + "type": "binary", + "datas": value, } fname = False if self.filename: fname = record[self.filename] - vals['datas_fname'] = fname + vals["datas_fname"] = fname if fname and self.storage_path: storage_key = self._build_storage_key(fname) if not fname: storage_key = False - env['ir.attachment'].sudo().with_context( + env["ir.attachment"].sudo().with_context( binary_field_real_user=env.user, storage_location=self.storage_location, force_storage_key=storage_key, @@ -80,21 +79,22 @@ def write(self, records, value): storage_location=self.storage_location, force_storage_key=storage_key, ), - value + value, ) return True def _setup_regular_base(self, model): - super()._setup_regular_base(model) + res = super()._setup_regular_base(model) if self.storage_path: - assert self.filename is not None, \ + assert self.filename is not None, ( "Field %s defines storage_path without filename" % self + ) + return res def _build_storage_key(self, filename): - return '/'.join([ - self.storage_path.rstrip('/'), - unicodedata.normalize('NFKC', filename) - ]) + return "/".join( + [self.storage_path.rstrip("/"), unicodedata.normalize("NFKC", filename)] + ) fields.FileURL = FileURL diff --git a/cloud_platform/__init__.py b/cloud_platform/__init__.py index a9e33722..0650744f 100644 --- a/cloud_platform/__init__.py +++ b/cloud_platform/__init__.py @@ -1,2 +1 @@ - from . import models diff --git a/cloud_platform/__manifest__.py b/cloud_platform/__manifest__.py index 054a507a..2559d986 100644 --- a/cloud_platform/__manifest__.py +++ b/cloud_platform/__manifest__.py @@ -2,19 +2,20 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{'name': 'Cloud Platform', - 'summary': 'Addons required for the Camptocamp Cloud Platform', - 'version': "15.0.2.0.0", - 'author': 'Camptocamp,Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'category': 'Extra Tools', - 'depends': [ - 'session_redis', - 'monitoring_status', - 'logging_json', - 'server_environment', # OCA/server-tools - ], - "website": "https://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Cloud Platform", + "summary": "Addons required for the Camptocamp Cloud Platform", + "version": "15.0.2.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": [ + "session_redis", + "monitoring_status", + "logging_json", + "server_environment", # OCA/server-tools + ], + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/cloud_platform/models/__init__.py b/cloud_platform/models/__init__.py index fe15395f..5d08f36c 100644 --- a/cloud_platform/models/__init__.py +++ b/cloud_platform/models/__init__.py @@ -1,2 +1 @@ - from . import cloud_platform diff --git a/cloud_platform/models/cloud_platform.py b/cloud_platform/models/cloud_platform.py index 750f4bd0..4d8c4990 100644 --- a/cloud_platform/models/cloud_platform.py +++ b/cloud_platform/models/cloud_platform.py @@ -4,46 +4,38 @@ import logging import os import re - from collections import namedtuple from distutils.util import strtobool from odoo import api, models from odoo.tools.config import config - _logger = logging.getLogger(__name__) def is_true(strval): - return bool(strtobool(strval or '0')) + return bool(strtobool(strval or "0")) -PlatformConfig = namedtuple( - 'PlatformConfig', - 'filestore' -) +PlatformConfig = namedtuple("PlatformConfig", "filestore") -FilestoreKind = namedtuple( - 'FilestoreKind', - ['name', 'location'] -) +FilestoreKind = namedtuple("FilestoreKind", ["name", "location"]) class CloudPlatform(models.AbstractModel): - _name = 'cloud.platform' - _description = 'cloud.platform' + _name = "cloud.platform" + _description = "cloud.platform" @api.model def _default_config(self): - return PlatformConfig(self._filestore_kinds()['db']) + return PlatformConfig(self._filestore_kinds()["db"]) @api.model def _filestore_kinds(self): return { - 'db': FilestoreKind('db', 'local'), - 'file': FilestoreKind('file', 'local'), + "db": FilestoreKind("db", "local"), + "file": FilestoreKind("file", "local"), } @api.model @@ -53,33 +45,31 @@ def _platform_kinds(self): @api.model def _config_by_server_env(self, platform_kind, environment): configs_getter = getattr( - self, - '_config_by_server_env_for_%s' % platform_kind, - None + self, "_config_by_server_env_for_%s" % platform_kind, None ) configs = configs_getter() if configs_getter else {} return configs.get(environment) or self._default_config() def _get_running_env(self): - environment_name = config['running_env'] - if environment_name.startswith('labs'): + environment_name = config["running_env"] + if environment_name.startswith("labs"): # We allow to have environments such as 'labs-logistics' # or 'labs-finance', in order to have the matching ribbon. - environment_name = 'labs' + environment_name = "labs" return environment_name @api.model def _install(self, platform_kind): assert platform_kind in self._platform_kinds() - params = self.env['ir.config_parameter'].sudo() - params.set_param('cloud.platform.kind', platform_kind) + params = self.env["ir.config_parameter"].sudo() + params.set_param("cloud.platform.kind", platform_kind) environment_name = self._get_running_env() configs = self._config_by_server_env(platform_kind, environment_name) - params.set_param('ir_attachment.location', configs.filestore.name) + params.set_param("ir_attachment.location", configs.filestore.name) self.check() - if configs.filestore.location == 'remote': - self.env['ir.attachment'].sudo().force_storage() - _logger.info('cloud platform configured for {}'.format(platform_kind)) + if configs.filestore.location == "remote": + self.env["ir.attachment"].sudo().force_storage() + _logger.info("cloud platform configured for {}".format(platform_kind)) @api.model def install(self): @@ -91,39 +81,39 @@ def _check_filestore(self, environment_name): @api.model def _check_redis(self, environment_name): - if environment_name in ('prod', 'integration', 'labs', 'test'): - assert is_true(os.environ.get('ODOO_SESSION_REDIS')), ( + if environment_name in ("prod", "integration", "labs", "test"): + assert is_true(os.environ.get("ODOO_SESSION_REDIS")), ( "Redis must be activated on prod, integration, labs," " test instances. This is done by setting ODOO_SESSION_REDIS=1." ) - assert (os.environ.get('ODOO_SESSION_REDIS_HOST') or - os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST') or - os.environ.get('ODOO_SESSION_REDIS_URL')), ( + assert ( + os.environ.get("ODOO_SESSION_REDIS_HOST") + or os.environ.get("ODOO_SESSION_REDIS_SENTINEL_HOST") + or os.environ.get("ODOO_SESSION_REDIS_URL") + ), ( "ODOO_SESSION_REDIS_HOST or " "ODOO_SESSION_REDIS_SENTINEL_HOST or " "ODOO_SESSION_REDIS_URL " "environment variable is required to connect on Redis" ) - assert os.environ.get('ODOO_SESSION_REDIS_PREFIX'), ( + assert os.environ.get("ODOO_SESSION_REDIS_PREFIX"), ( "ODOO_SESSION_REDIS_PREFIX environment variable is required " "to store sessions on Redis" ) - prefix = os.environ['ODOO_SESSION_REDIS_PREFIX'] - assert re.match(r'^[a-z-0-9]+-odoo-[a-z-0-9]+$', prefix), ( + prefix = os.environ["ODOO_SESSION_REDIS_PREFIX"] + assert re.match(r"^[a-z-0-9]+-odoo-[a-z-0-9]+$", prefix), ( "ODOO_SESSION_REDIS_PREFIX must match '-odoo-'" ", we got: '%s'" % (prefix,) ) @api.model def check(self): - if is_true(os.environ.get('ODOO_CLOUD_PLATFORM_UNSAFE')): - _logger.warning( - "cloud platform checks disabled, this is not safe" - ) + if is_true(os.environ.get("ODOO_CLOUD_PLATFORM_UNSAFE")): + _logger.warning("cloud platform checks disabled, this is not safe") return - params = self.env['ir.config_parameter'].sudo() - kind = params.get_param('cloud.platform.kind') + params = self.env["ir.config_parameter"].sudo() + kind = params.get_param("cloud.platform.kind") if not kind: _logger.warning( "cloud platform not configured, you should " diff --git a/cloud_platform/songs.py b/cloud_platform/songs.py index 043fc7ba..1a0f7888 100644 --- a/cloud_platform/songs.py +++ b/cloud_platform/songs.py @@ -1,3 +1,2 @@ - def install(ctx): - ctx.env['cloud.platform'].install() + ctx.env["cloud.platform"].install() diff --git a/cloud_platform_azure/README.md b/cloud_platform_azure/README.md index 1f7bd5d1..449ab29b 100644 --- a/cloud_platform_azure/README.md +++ b/cloud_platform_azure/README.md @@ -1,5 +1,4 @@ -Cloud Platform Azure -==================== +# Cloud Platform Azure Install addons specific to the Azure setup. diff --git a/cloud_platform_azure/__manifest__.py b/cloud_platform_azure/__manifest__.py index 5cfa3737..b478a066 100644 --- a/cloud_platform_azure/__manifest__.py +++ b/cloud_platform_azure/__manifest__.py @@ -18,7 +18,7 @@ "cloud_platform_ovh", "cloud_platform_exoscale", ], - "website": "https://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": True, } diff --git a/cloud_platform_azure/models/cloud_platform.py b/cloud_platform_azure/models/cloud_platform.py index e93e706a..b72d2294 100644 --- a/cloud_platform_azure/models/cloud_platform.py +++ b/cloud_platform_azure/models/cloud_platform.py @@ -1,13 +1,15 @@ # Copyright 2016-2021 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import re import os +import re -from odoo import models, api -from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind -from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig +from odoo import api, models +from odoo.addons.cloud_platform.models.cloud_platform import ( + FilestoreKind, + PlatformConfig, +) AZURE_STORE_KIND = FilestoreKind("azure", "remote") @@ -42,8 +44,7 @@ def _config_by_server_env_for_azure(self): @api.model def _check_filestore(self, environment_name): params = self.env["ir.config_parameter"].sudo() - use_azure = (params.get_param("ir_attachment.location") == - AZURE_STORE_KIND.name) + use_azure = params.get_param("ir_attachment.location") == AZURE_STORE_KIND.name if environment_name in ("prod", "integration"): # Labs instances use azure by default, but we don't want # to enforce it in case we want to test something with a different diff --git a/cloud_platform_exoscale/README.md b/cloud_platform_exoscale/README.md index f2931eaf..3c815ed0 100644 --- a/cloud_platform_exoscale/README.md +++ b/cloud_platform_exoscale/README.md @@ -1,5 +1,4 @@ -Cloud Platform Exoscale -======================= +# Cloud Platform Exoscale Install addons specific to the Exoscale setup. diff --git a/cloud_platform_exoscale/__manifest__.py b/cloud_platform_exoscale/__manifest__.py index 8cca042c..94b126bb 100644 --- a/cloud_platform_exoscale/__manifest__.py +++ b/cloud_platform_exoscale/__manifest__.py @@ -17,7 +17,7 @@ "excludes": [ "cloud_platform_ovh", ], - "website": "https://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": True, } diff --git a/cloud_platform_exoscale/models/cloud_platform.py b/cloud_platform_exoscale/models/cloud_platform.py index 3a29942b..66702fdb 100644 --- a/cloud_platform_exoscale/models/cloud_platform.py +++ b/cloud_platform_exoscale/models/cloud_platform.py @@ -1,50 +1,51 @@ # Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import re import os +import re -from odoo import models, api -from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind -from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig +from odoo import api, models +from odoo.addons.cloud_platform.models.cloud_platform import ( + FilestoreKind, + PlatformConfig, +) -S3_STORE_KIND = FilestoreKind('s3', 'remote') +S3_STORE_KIND = FilestoreKind("s3", "remote") class CloudPlatform(models.AbstractModel): - _inherit = 'cloud.platform' + _inherit = "cloud.platform" @api.model def _filestore_kinds(self): kinds = super(CloudPlatform, self)._filestore_kinds() - kinds['s3'] = S3_STORE_KIND + kinds["s3"] = S3_STORE_KIND return kinds @api.model def _platform_kinds(self): kinds = super(CloudPlatform, self)._platform_kinds() - kinds.append('exoscale') + kinds.append("exoscale") return kinds @api.model def _config_by_server_env_for_exoscale(self): fs_kinds = self._filestore_kinds() configs = { - 'prod': PlatformConfig(filestore=fs_kinds['s3']), - 'integration': PlatformConfig(filestore=fs_kinds['s3']), - 'labs': PlatformConfig(filestore=fs_kinds['s3']), - 'test': PlatformConfig(filestore=fs_kinds['db']), - 'dev': PlatformConfig(filestore=fs_kinds['db']), + "prod": PlatformConfig(filestore=fs_kinds["s3"]), + "integration": PlatformConfig(filestore=fs_kinds["s3"]), + "labs": PlatformConfig(filestore=fs_kinds["s3"]), + "test": PlatformConfig(filestore=fs_kinds["db"]), + "dev": PlatformConfig(filestore=fs_kinds["db"]), } return configs @api.model def _check_filestore(self, environment_name): - params = self.env['ir.config_parameter'].sudo() - use_s3 = (params.get_param('ir_attachment.location') == - S3_STORE_KIND.name) - if environment_name in ('prod', 'integration'): + params = self.env["ir.config_parameter"].sudo() + use_s3 = params.get_param("ir_attachment.location") == S3_STORE_KIND.name + if environment_name in ("prod", "integration"): # Labs instances use s3 by default, but we don't want # to enforce it in case we want to test something with a different # storage. At your own risks! @@ -55,16 +56,16 @@ def _check_filestore(self, environment_name): "automatically." ) if use_s3: - assert os.environ.get('AWS_ACCESS_KEY_ID'), ( + assert os.environ.get("AWS_ACCESS_KEY_ID"), ( "AWS_ACCESS_KEY_ID environment variable is required when " "ir_attachment.location is 's3'." ) - assert os.environ.get('AWS_SECRET_ACCESS_KEY'), ( + assert os.environ.get("AWS_SECRET_ACCESS_KEY"), ( "AWS_SECRET_ACCESS_KEY environment variable is required when " "ir_attachment.location is 's3'." ) - bucket_name = os.environ.get('AWS_BUCKETNAME', '') - if environment_name in ('prod', 'integration', 'labs'): + bucket_name = os.environ.get("AWS_BUCKETNAME", "") + if environment_name in ("prod", "integration", "labs"): assert bucket_name, ( "AWS_BUCKETNAME environment variable is required when " "ir_attachment.location is 's3'.\n" @@ -80,10 +81,10 @@ def _check_filestore(self, environment_name): # # Use AWS_BUCKETNAME_UNSTRUCTURED to by-pass check on bucket name # structure - if os.environ.get('AWS_BUCKETNAME_UNSTRUCTURED'): + if os.environ.get("AWS_BUCKETNAME_UNSTRUCTURED"): return - prod_bucket = bool(re.match(r'[a-z-0-9]+-odoo-prod', bucket_name)) - if environment_name == 'prod': + prod_bucket = bool(re.match(r"[a-z-0-9]+-odoo-prod", bucket_name)) + if environment_name == "prod": assert prod_bucket, ( "AWS_BUCKETNAME should match '-odoo-prod', " "we got: '%s'" % (bucket_name,) @@ -96,9 +97,9 @@ def _check_filestore(self, environment_name): "we got: '%s'" % (bucket_name,) ) - elif environment_name == 'test': + elif environment_name == "test": # store in DB so we don't have files local to the host - assert params.get_param('ir_attachment.location') == 'db', ( + assert params.get_param("ir_attachment.location") == "db", ( "In test instances, files must be stored in the database with " "'ir_attachment.location' set to 'db'. This is " "automatically set by the function 'install()'." @@ -106,4 +107,4 @@ def _check_filestore(self, environment_name): @api.model def install(self): - self._install('exoscale') + self._install("exoscale") diff --git a/cloud_platform_ovh/README.md b/cloud_platform_ovh/README.md index c350eba0..f82fe48d 100644 --- a/cloud_platform_ovh/README.md +++ b/cloud_platform_ovh/README.md @@ -1,7 +1,5 @@ -Cloud Platform OVH -================== +# Cloud Platform OVH Install addons specific to the OVH setup. * The object storage is Swift - diff --git a/cloud_platform_ovh/__manifest__.py b/cloud_platform_ovh/__manifest__.py index 9945f50b..695524c2 100644 --- a/cloud_platform_ovh/__manifest__.py +++ b/cloud_platform_ovh/__manifest__.py @@ -17,7 +17,7 @@ "excludes": [ "cloud_platform_exoscale", ], - "website": "https://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": True, } diff --git a/cloud_platform_ovh/models/cloud_platform.py b/cloud_platform_ovh/models/cloud_platform.py index c4e02165..bed4db9a 100644 --- a/cloud_platform_ovh/models/cloud_platform.py +++ b/cloud_platform_ovh/models/cloud_platform.py @@ -1,51 +1,51 @@ # Copyright 2017-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import re import os +import re from odoo import api, models -from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind -from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig - +from odoo.addons.cloud_platform.models.cloud_platform import ( + FilestoreKind, + PlatformConfig, +) -SWIFT_STORE_KIND = FilestoreKind('swift', 'remote') +SWIFT_STORE_KIND = FilestoreKind("swift", "remote") class CloudPlatform(models.AbstractModel): - _inherit = 'cloud.platform' + _inherit = "cloud.platform" @api.model def _filestore_kinds(self): kinds = super(CloudPlatform, self)._filestore_kinds() - kinds['swift'] = SWIFT_STORE_KIND + kinds["swift"] = SWIFT_STORE_KIND return kinds @api.model def _platform_kinds(self): kinds = super()._platform_kinds() - kinds.append('ovh') + kinds.append("ovh") return kinds @api.model def _config_by_server_env_for_ovh(self): fs_kinds = self._filestore_kinds() configs = { - 'prod': PlatformConfig(filestore=fs_kinds['swift']), - 'integration': PlatformConfig(filestore=fs_kinds['swift']), - 'labs': PlatformConfig(filestore=fs_kinds['swift']), - 'test': PlatformConfig(filestore=fs_kinds['db']), - 'dev': PlatformConfig(filestore=fs_kinds['db']), + "prod": PlatformConfig(filestore=fs_kinds["swift"]), + "integration": PlatformConfig(filestore=fs_kinds["swift"]), + "labs": PlatformConfig(filestore=fs_kinds["swift"]), + "test": PlatformConfig(filestore=fs_kinds["db"]), + "dev": PlatformConfig(filestore=fs_kinds["db"]), } return configs @api.model def _check_filestore(self, environment_name): - params = self.env['ir.config_parameter'].sudo() - use_swift = (params.get_param('ir_attachment.location') == - SWIFT_STORE_KIND.name) - if environment_name in ('prod', 'integration'): + params = self.env["ir.config_parameter"].sudo() + use_swift = params.get_param("ir_attachment.location") == SWIFT_STORE_KIND.name + if environment_name in ("prod", "integration"): # Labs instances use swift by default, but we don't want # to enforce it in case we want to test something with a different # storage. At your own risks! @@ -56,20 +56,20 @@ def _check_filestore(self, environment_name): "automatically." ) if use_swift: - assert os.environ.get('SWIFT_AUTH_URL'), ( + assert os.environ.get("SWIFT_AUTH_URL"), ( "SWIFT_AUTH_URL environment variable is required when " "ir_attachment.location is 'swift'." ) - assert os.environ.get('SWIFT_ACCOUNT'), ( + assert os.environ.get("SWIFT_ACCOUNT"), ( "SWIFT_ACCOUNT environment variable is required when " "ir_attachment.location is 'swift'." ) - assert os.environ.get('SWIFT_PASSWORD'), ( + assert os.environ.get("SWIFT_PASSWORD"), ( "SWIFT_PASSWORD environment variable is required when " "ir_attachment.location is 'swift'." ) - container_name = os.environ.get('SWIFT_WRITE_CONTAINER', '') - if environment_name in ('prod', 'integration', 'labs'): + container_name = os.environ.get("SWIFT_WRITE_CONTAINER", "") + if environment_name in ("prod", "integration", "labs"): assert container_name, ( "SWIFT_WRITE_CONTAINER environment variable is required when " "ir_attachment.location is 'swift'.\n" @@ -80,16 +80,15 @@ def _check_filestore(self, environment_name): "If you don't actually need a bucket, change the" " 'ir_attachment.location' parameter." ) - prod_container = bool(re.match(r'[a-z0-9-]+-odoo-prod', - container_name)) + prod_container = bool(re.match(r"[a-z0-9-]+-odoo-prod", container_name)) # A bucket name is defined under the following format # -odoo- # # Use SWIFT_WRITE_CONTAINER_UNSTRUCTURED to by-pass check on bucket name # structure - if os.environ.get('SWIFT_WRITE_CONTAINER_UNSTRUCTURED'): + if os.environ.get("SWIFT_WRITE_CONTAINER_UNSTRUCTURED"): return - if environment_name == 'prod': + if environment_name == "prod": assert prod_container, ( "SWIFT_WRITE_CONTAINER should match '-odoo-prod', " "we got: '%s'" % (container_name,) @@ -101,9 +100,9 @@ def _check_filestore(self, environment_name): "SWIFT_WRITE_CONTAINER should not match " "'-odoo-prod', we got: '%s'" % (container_name,) ) - elif environment_name == 'test': + elif environment_name == "test": # store in DB so we don't have files local to the host - assert params.get_param('ir_attachment.location') == 'db', ( + assert params.get_param("ir_attachment.location") == "db", ( "In test instances, files must be stored in the database with " "'ir_attachment.location' set to 'db'. This is " "automatically set by the function 'install()'." @@ -111,4 +110,4 @@ def _check_filestore(self, environment_name): @api.model def install(self): - self._install('ovh') + self._install("ovh") diff --git a/logging_json/__init__.py b/logging_json/__init__.py index 1f1aa382..9f2ed663 100644 --- a/logging_json/__init__.py +++ b/logging_json/__init__.py @@ -1,2 +1 @@ - from . import json_log diff --git a/logging_json/__manifest__.py b/logging_json/__manifest__.py index b60a0096..2e594068 100644 --- a/logging_json/__manifest__.py +++ b/logging_json/__manifest__.py @@ -1,17 +1,19 @@ # Copyright 2016-2021 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "JSON Logging", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "Extra Tools", - "depends": ["base", - ], - "external_dependencies": { - "python": ["python-json-logger"], - }, - "website": "http://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "JSON Logging", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": [ + "base", + ], + "external_dependencies": { + "python": ["python-json-logger"], + }, + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/logging_json/json_log.py b/logging_json/json_log.py index 8215df47..09d3cbce 100644 --- a/logging_json/json_log.py +++ b/logging_json/json_log.py @@ -5,7 +5,6 @@ import os import threading import uuid - from distutils.util import strtobool from odoo import http @@ -20,23 +19,22 @@ def is_true(strval): - return bool(strtobool(strval or '0'.lower())) + return bool(strtobool(strval or "0".lower())) class OdooJsonFormatter(jsonlogger.JsonFormatter): - def add_fields(self, log_record, record, message_dict): record.pid = os.getpid() - record.dbname = getattr(threading.currentThread(), 'dbname', '?') - record.request_id = getattr(threading.current_thread(), 'request_uuid', None) - record.uid = getattr(threading.current_thread(), 'uid', None) + record.dbname = getattr(threading.currentThread(), "dbname", "?") + record.request_id = getattr(threading.current_thread(), "request_uuid", None) + record.uid = getattr(threading.current_thread(), "uid", None) _super = super(OdooJsonFormatter, self) return _super.add_fields(log_record, record, message_dict) -if is_true(os.environ.get('ODOO_LOGGING_JSON')): +if is_true(os.environ.get("ODOO_LOGGING_JSON")): formatted_message = ( - '%(asctime)s %(pid)s %(levelname)s %(dbname)s %(name)s: %(message)s' + "%(asctime)s %(pid)s %(levelname)s %(dbname)s %(name)s: %(message)s" ) formatter = OdooJsonFormatter(formatted_message) logging.getLogger().handlers[0].formatter = formatter diff --git a/monitoring_log_requests/__init__.py b/monitoring_log_requests/__init__.py index a9e33722..0650744f 100644 --- a/monitoring_log_requests/__init__.py +++ b/monitoring_log_requests/__init__.py @@ -1,2 +1 @@ - from . import models diff --git a/monitoring_log_requests/__manifest__.py b/monitoring_log_requests/__manifest__.py index 483ed1fd..17f24b78 100644 --- a/monitoring_log_requests/__manifest__.py +++ b/monitoring_log_requests/__manifest__.py @@ -2,13 +2,14 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Monitoring: Requests Logging", - "version": "15.0.1.0.0", - "author": "Camptocamp,Numigi,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "category", - "depends": ["base", "web"], - "website": "http://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Monitoring: Requests Logging", + "version": "15.0.1.0.0", + "author": "Camptocamp,Numigi,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "category", + "depends": ["base", "web"], + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/monitoring_log_requests/models/__init__.py b/monitoring_log_requests/models/__init__.py index 0d643ba0..9a5eb718 100644 --- a/monitoring_log_requests/models/__init__.py +++ b/monitoring_log_requests/models/__init__.py @@ -1,2 +1 @@ - from . import ir_http diff --git a/monitoring_log_requests/models/ir_http.py b/monitoring_log_requests/models/ir_http.py index abaf46b5..505e71fd 100644 --- a/monitoring_log_requests/models/ir_http.py +++ b/monitoring_log_requests/models/ir_http.py @@ -9,28 +9,28 @@ from odoo.http import request as http_request from odoo.tools.config import config - -_logger = logging.getLogger('monitoring.http.requests') +_logger = logging.getLogger("monitoring.http.requests") class IrHttp(models.AbstractModel): - _inherit = 'ir.http' + _inherit = "ir.http" @classmethod def _dispatch(cls): begin = time.time() response = super()._dispatch() end = time.time() - if (not cls._monitoring_blacklist(http_request) and - cls._monitoring_filter(http_request)): + if not cls._monitoring_blacklist(http_request) and cls._monitoring_filter( + http_request + ): info = cls._monitoring_info(http_request, response, begin, end) cls._monitoring_log(info) return response @classmethod def _monitoring_blacklist(cls, request): - path_info = request.httprequest.environ.get('PATH_INFO') - if path_info.startswith('/longpolling/'): + path_info = request.httprequest.environ.get("PATH_INFO") + if path_info.startswith("/longpolling/"): return True return False @@ -40,42 +40,45 @@ def _monitoring_filter(cls, _): @classmethod def _monitoring_info(cls, request, response, begin, end): - path = request.httprequest.environ.get('PATH_INFO') + path = request.httprequest.environ.get("PATH_INFO") info = { # timing - 'start_time': time.strftime("%Y-%m-%d %H:%M:%S", - time.gmtime(begin)), - 'duration': end - begin, + "start_time": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(begin)), + "duration": end - begin, # HTTP things - 'method': request.httprequest.method, - 'url': request.httprequest.url, - 'path': path, - 'content_type': request.httprequest.environ.get('CONTENT_TYPE'), - 'user_agent': request.httprequest.environ.get('HTTP_USER_AGENT'), + "method": request.httprequest.method, + "url": request.httprequest.url, + "path": path, + "content_type": request.httprequest.environ.get("CONTENT_TYPE"), + "user_agent": request.httprequest.environ.get("HTTP_USER_AGENT"), # Odoo things - 'db': None, - 'uid': request.uid, - 'login': None, - 'server_environment': config.get('running_env'), - 'model': None, - 'model_method': None, - 'workflow_signal': None, + "db": None, + "uid": request.uid, + "login": None, + "server_environment": config.get("running_env"), + "model": None, + "model_method": None, + "workflow_signal": None, # response things - 'response_status_code': None, + "response_status_code": None, } - if hasattr(request, 'status_code'): - info['status_code'] = response.status_code - if hasattr(request, 'session'): - info.update({ - 'login': request.session.get('login'), - 'db': request.session.get('db'), - }) - if hasattr(request, 'params'): - info.update({ - 'model': request.params.get('model'), - 'model_method': request.params.get('method'), - 'workflow_signal': request.params.get('signal'), - }) + if hasattr(request, "status_code"): + info["status_code"] = response.status_code + if hasattr(request, "session"): + info.update( + { + "login": request.session.get("login"), + "db": request.session.get("db"), + } + ) + if hasattr(request, "params"): + info.update( + { + "model": request.params.get("model"), + "model_method": request.params.get("method"), + "workflow_signal": request.params.get("signal"), + } + ) return info @classmethod diff --git a/monitoring_prometheus/__manifest__.py b/monitoring_prometheus/__manifest__.py index 260570c7..f6b7eb0a 100644 --- a/monitoring_prometheus/__manifest__.py +++ b/monitoring_prometheus/__manifest__.py @@ -13,7 +13,7 @@ "web", "server_environment", ], - "website": "http://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "external_dependencies": { "python": ["prometheus_client"], diff --git a/monitoring_prometheus/controllers/prometheus_metrics.py b/monitoring_prometheus/controllers/prometheus_metrics.py index 411a2acf..4aa49960 100644 --- a/monitoring_prometheus/controllers/prometheus_metrics.py +++ b/monitoring_prometheus/controllers/prometheus_metrics.py @@ -1,11 +1,12 @@ # Copyright 2016-2021 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo.http import Controller, route from prometheus_client import generate_latest +from odoo.http import Controller, route + class PrometheusController(Controller): - @route('/metrics', auth='public') + @route("/metrics", auth="public") def metrics(self): return generate_latest() diff --git a/monitoring_prometheus/models/ir_http.py b/monitoring_prometheus/models/ir_http.py index 0c026d6d..26375af8 100644 --- a/monitoring_prometheus/models/ir_http.py +++ b/monitoring_prometheus/models/ir_http.py @@ -1,10 +1,10 @@ # Copyright 2016-2021 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from prometheus_client import Counter, Summary + from odoo import models from odoo.http import request -from prometheus_client import Summary, Counter - REQUEST_TIME = Summary( "request_latency_sec", "Request response time in sec", ["query_type"] diff --git a/monitoring_statsd/__init__.py b/monitoring_statsd/__init__.py index a9e33722..0650744f 100644 --- a/monitoring_statsd/__init__.py +++ b/monitoring_statsd/__init__.py @@ -1,2 +1 @@ - from . import models diff --git a/monitoring_statsd/__manifest__.py b/monitoring_statsd/__manifest__.py index 0bc00e49..17a265f5 100644 --- a/monitoring_statsd/__manifest__.py +++ b/monitoring_statsd/__manifest__.py @@ -2,19 +2,21 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Monitoring: Statsd Metrics", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "category", - "depends": ["base", - "web", - "server_environment", - ], - "website": "http://www.camptocamp.com", - "data": [], - "external_dependencies": { - "python": ["statsd"], - }, - "installable": True, - } +{ + "name": "Monitoring: Statsd Metrics", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "category", + "depends": [ + "base", + "web", + "server_environment", + ], + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "external_dependencies": { + "python": ["statsd"], + }, + "installable": True, +} diff --git a/monitoring_statsd/models/__init__.py b/monitoring_statsd/models/__init__.py index 0d643ba0..9a5eb718 100644 --- a/monitoring_statsd/models/__init__.py +++ b/monitoring_statsd/models/__init__.py @@ -1,2 +1 @@ - from . import ir_http diff --git a/monitoring_statsd/models/ir_http.py b/monitoring_statsd/models/ir_http.py index ba4f5387..e49120c5 100644 --- a/monitoring_statsd/models/ir_http.py +++ b/monitoring_statsd/models/ir_http.py @@ -4,38 +4,46 @@ from odoo import models from odoo.http import request -from ..statsd_client import statsd, customer, environment +from ..statsd_client import customer, environment, statsd class IrHttp(models.AbstractModel): - _inherit = 'ir.http' + _inherit = "ir.http" @classmethod def _dispatch(cls): if not statsd: return super()._dispatch() - path_info = request.httprequest.environ.get('PATH_INFO') - if path_info.startswith('/longpolling/'): + path_info = request.httprequest.environ.get("PATH_INFO") + if path_info.startswith("/longpolling/"): return super()._dispatch() - parts = ['http', ] - if path_info.startswith('/web/dataset/call_button'): - parts += ['button', - customer, environment, - request.params['model'].replace('.', '_'), - request.params['method'], - ] - elif path_info.startswith('/web/dataset/exec_workflow'): - parts += ['workflow', - customer, environment, - request.params['model'].replace('.', '_'), - request.params['signal'], - ] + parts = [ + "http", + ] + if path_info.startswith("/web/dataset/call_button"): + parts += [ + "button", + customer, + environment, + request.params["model"].replace(".", "_"), + request.params["method"], + ] + elif path_info.startswith("/web/dataset/exec_workflow"): + parts += [ + "workflow", + customer, + environment, + request.params["model"].replace(".", "_"), + request.params["signal"], + ] else: - parts += ['request', - customer, environment, - ] + parts += [ + "request", + customer, + environment, + ] - with statsd.timer('.'.join(parts)): + with statsd.timer(".".join(parts)): return super()._dispatch() diff --git a/monitoring_statsd/statsd_client.py b/monitoring_statsd/statsd_client.py index 1ab1bba3..9231fe9b 100644 --- a/monitoring_statsd/statsd_client.py +++ b/monitoring_statsd/statsd_client.py @@ -3,7 +3,6 @@ import logging import os - from distutils.util import strtobool from odoo.tools.config import config @@ -14,40 +13,39 @@ from statsd import defaults from statsd.client import StatsClient except ImportError: - _logger.warning('statds must be installed') + _logger.warning("statds must be installed") defaults = None # noqa StatsClient = None # noqa def is_true(strval): - return bool(strtobool(strval or '0'.lower())) + return bool(strtobool(strval or "0".lower())) -statsd_active = is_true(os.environ.get('ODOO_STATSD')) +statsd_active = is_true(os.environ.get("ODOO_STATSD")) statsd = None customer = None environment = None if statsd_active and statsd is None and StatsClient is not None: - if not os.environ.get('STATSD_CUSTOMER'): - raise Exception( - 'STATSD_CUSTOMER must contain the name of the customer' - ) - customer = os.environ.get('STATSD_CUSTOMER') - if os.environ.get('STATSD_ENVIRONMENT'): - environment = os.environ['STATSD_ENVIRONMENT'] - elif config.get('running_env'): - environment = config['running_env'] + if not os.environ.get("STATSD_CUSTOMER"): + raise Exception("STATSD_CUSTOMER must contain the name of the customer") + customer = os.environ.get("STATSD_CUSTOMER") + if os.environ.get("STATSD_ENVIRONMENT"): + environment = os.environ["STATSD_ENVIRONMENT"] + elif config.get("running_env"): + environment = config["running_env"] else: raise Exception( - 'Either STATSD_ENVIRONMENT or configuration option running_env ' - 'must contain the environment (prod, integration, ...)' + "Either STATSD_ENVIRONMENT or configuration option running_env " + "must contain the environment (prod, integration, ...)" ) - host = os.getenv('STATSD_HOST', defaults.HOST) - port = int(os.getenv('STATSD_PORT', defaults.PORT)) - prefix = os.getenv('STATSD_PREFIX', defaults.PREFIX) - maxudpsize = int(os.getenv('STATSD_MAXUDPSIZE', defaults.MAXUDPSIZE)) - ipv6 = bool(int(os.getenv('STATSD_IPV6', defaults.IPV6))) - statsd = StatsClient(host=host, port=port, prefix='odoo', - maxudpsize=maxudpsize, ipv6=ipv6) + host = os.getenv("STATSD_HOST", defaults.HOST) + port = int(os.getenv("STATSD_PORT", defaults.PORT)) + prefix = os.getenv("STATSD_PREFIX", defaults.PREFIX) + maxudpsize = int(os.getenv("STATSD_MAXUDPSIZE", defaults.MAXUDPSIZE)) + ipv6 = bool(int(os.getenv("STATSD_IPV6", defaults.IPV6))) + statsd = StatsClient( + host=host, port=port, prefix="odoo", maxudpsize=maxudpsize, ipv6=ipv6 + ) diff --git a/monitoring_status/__manifest__.py b/monitoring_status/__manifest__.py index 42ba1459..7becc42d 100644 --- a/monitoring_status/__manifest__.py +++ b/monitoring_status/__manifest__.py @@ -2,13 +2,14 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Monitoring: Status", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "category", - "depends": ["base", "web"], - "website": "http://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Monitoring: Status", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "category", + "depends": ["base", "web"], + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/monitoring_status/controllers/main.py b/monitoring_status/controllers/main.py index c2463eb4..21b6893c 100644 --- a/monitoring_status/controllers/main.py +++ b/monitoring_status/controllers/main.py @@ -1,18 +1,18 @@ # Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import logging import json +import logging import werkzeug from odoo import http + from odoo.addons.web.controllers.main import ensure_db class HealthCheckFilter(logging.Filter): - - def __init__(self, path, name=''): + def __init__(self, path, name=""): super().__init__(name) self.path = path @@ -20,20 +20,19 @@ def filter(self, record): return self.path not in record.getMessage() -logging.getLogger('werkzeug').addFilter( - HealthCheckFilter('GET /monitoring/status HTTP') +logging.getLogger("werkzeug").addFilter( + HealthCheckFilter("GET /monitoring/status HTTP") ) class Monitoring(http.Controller): - - @http.route('/monitoring/status', type='http', auth='none') + @http.route("/monitoring/status", type="http", auth="none") def status(self): ensure_db() # TODO: add 'sub-systems' status and infos: # queue job, cron, database, ... - headers = {'Content-Type': 'application/json'} - info = {'status': 1} + headers = {"Content-Type": "application/json"} + info = {"status": 1} session = http.request.session # We set a custom expiration of 1 second for this request, as we do a # lot of health checks, we don't want those anonymous sessions to be diff --git a/session_redis/__init__.py b/session_redis/__init__.py index 8f2ee9ec..a64bcb4b 100644 --- a/session_redis/__init__.py +++ b/session_redis/__init__.py @@ -1,3 +1,2 @@ - from . import http from . import session diff --git a/session_redis/__manifest__.py b/session_redis/__manifest__.py index 83086567..c291bbc8 100644 --- a/session_redis/__manifest__.py +++ b/session_redis/__manifest__.py @@ -2,17 +2,18 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -{"name": "Sessions in Redis", - "summary": "Store web sessions in Redis", - "version": "15.0.1.0.0", - "author": "Camptocamp,Odoo Community Association (OCA)", - "license": "AGPL-3", - "category": "Extra Tools", - "depends": ["base"], - "external_dependencies": { - "python": ["redis"], - }, - "website": "http://www.camptocamp.com", - "data": [], - "installable": True, - } +{ + "name": "Sessions in Redis", + "summary": "Store web sessions in Redis", + "version": "15.0.1.0.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": ["base"], + "external_dependencies": { + "python": ["redis"], + }, + "website": "https://github.com/camptocamp/odoo-cloud-platform", + "data": [], + "installable": True, +} diff --git a/session_redis/http.py b/session_redis/http.py index 48902af0..904546a1 100644 --- a/session_redis/http.py +++ b/session_redis/http.py @@ -3,7 +3,6 @@ import logging import os - from distutils.util import strtobool import odoo @@ -23,46 +22,46 @@ def is_true(strval): - return bool(strtobool(strval or '0'.lower())) + return bool(strtobool(strval or "0".lower())) -sentinel_host = os.environ.get('ODOO_SESSION_REDIS_SENTINEL_HOST') -sentinel_master_name = os.environ.get( - 'ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME' -) +sentinel_host = os.environ.get("ODOO_SESSION_REDIS_SENTINEL_HOST") +sentinel_master_name = os.environ.get("ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME") if sentinel_host and not sentinel_master_name: raise Exception( "ODOO_SESSION_REDIS_SENTINEL_MASTER_NAME must be defined " "when using session_redis" ) -sentinel_port = int(os.environ.get('ODOO_SESSION_REDIS_SENTINEL_PORT', 26379)) -host = os.environ.get('ODOO_SESSION_REDIS_HOST', 'localhost') -port = int(os.environ.get('ODOO_SESSION_REDIS_PORT', 6379)) -prefix = os.environ.get('ODOO_SESSION_REDIS_PREFIX') -url = os.environ.get('ODOO_SESSION_REDIS_URL') -password = os.environ.get('ODOO_SESSION_REDIS_PASSWORD') -expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION') -anon_expiration = os.environ.get('ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS') +sentinel_port = int(os.environ.get("ODOO_SESSION_REDIS_SENTINEL_PORT", 26379)) +host = os.environ.get("ODOO_SESSION_REDIS_HOST", "localhost") +port = int(os.environ.get("ODOO_SESSION_REDIS_PORT", 6379)) +prefix = os.environ.get("ODOO_SESSION_REDIS_PREFIX") +url = os.environ.get("ODOO_SESSION_REDIS_URL") +password = os.environ.get("ODOO_SESSION_REDIS_PASSWORD") +expiration = os.environ.get("ODOO_SESSION_REDIS_EXPIRATION") +anon_expiration = os.environ.get("ODOO_SESSION_REDIS_EXPIRATION_ANONYMOUS") @lazy_property def session_store(self): if sentinel_host: - sentinel = Sentinel([(sentinel_host, sentinel_port)], - password=password) + sentinel = Sentinel([(sentinel_host, sentinel_port)], password=password) redis_client = sentinel.master_for(sentinel_master_name) elif url: redis_client = redis.from_url(url) else: redis_client = redis.Redis(host=host, port=port, password=password) - return RedisSessionStore(redis=redis_client, prefix=prefix, - expiration=expiration, - anon_expiration=anon_expiration, - session_class=http.OpenERPSession) + return RedisSessionStore( + redis=redis_client, + prefix=prefix, + expiration=expiration, + anon_expiration=anon_expiration, + session_class=http.OpenERPSession, + ) def session_gc(session_store): - """ Do not garbage collect the sessions + """Do not garbage collect the sessions Redis keys are automatically cleaned at the end of their expiration. @@ -76,17 +75,25 @@ def purge_fs_sessions(path): try: os.unlink(path) except OSError: - pass + _logger.warning("OS Error during purge of redis sessions.") -if is_true(os.environ.get('ODOO_SESSION_REDIS')): +if is_true(os.environ.get("ODOO_SESSION_REDIS")): if sentinel_host: - _logger.debug("HTTP sessions stored in Redis with prefix '%s'. " - "Using Sentinel on %s:%s", - prefix or '', sentinel_host, sentinel_port) + _logger.debug( + "HTTP sessions stored in Redis with prefix '%s'. " + "Using Sentinel on %s:%s", + prefix or "", + sentinel_host, + sentinel_port, + ) else: - _logger.debug("HTTP sessions stored in Redis with prefix '%s' on " - "%s:%s", prefix or '', host, port) + _logger.debug( + "HTTP sessions stored in Redis with prefix '%s' on " "%s:%s", + prefix or "", + host, + port, + ) http.Root.session_store = session_store http.session_gc = session_gc diff --git a/session_redis/json_encoding.py b/session_redis/json_encoding.py index f535a8f0..bb1c57a1 100644 --- a/session_redis/json_encoding.py +++ b/session_redis/json_encoding.py @@ -2,7 +2,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import json - from datetime import date, datetime import dateutil diff --git a/session_redis/session.py b/session_redis/session.py index 4c8457e6..dce27740 100644 --- a/session_redis/session.py +++ b/session_redis/session.py @@ -17,10 +17,16 @@ class RedisSessionStore(SessionStore): - """ SessionStore that saves session to redis """ - - def __init__(self, redis, session_class=None, - prefix='', expiration=None, anon_expiration=None): + """SessionStore that saves session to redis""" + + def __init__( + self, + redis, + session_class=None, + prefix="", + expiration=None, + anon_expiration=None, + ): super().__init__(session_class=session_class) self.redis = redis if expiration is None: @@ -31,14 +37,12 @@ def __init__(self, redis, session_class=None, self.anon_expiration = DEFAULT_SESSION_TIMEOUT_ANONYMOUS else: self.anon_expiration = anon_expiration - self.prefix = 'session:' + self.prefix = "session:" if prefix: - self.prefix = '%s:%s:' % ( - self.prefix, prefix - ) + self.prefix = "%s:%s:" % (self.prefix, prefix) def build_key(self, sid): - return '%s%s' % (self.prefix, sid) + return "%s%s" % (self.prefix, sid) def save(self, session): key = self.build_key(session.sid) @@ -51,48 +55,56 @@ def save(self, session): expiration = session.expiration or self.anon_expiration if _logger.isEnabledFor(logging.DEBUG): if session.uid: - user_msg = "user '%s' (id: %s)" % ( - session.login, session.uid) + user_msg = "user '%s' (id: %s)" % (session.login, session.uid) else: user_msg = "anonymous user" - _logger.debug("saving session with key '%s' and " - "expiration of %s seconds for %s", - key, expiration, user_msg) + _logger.debug( + "saving session with key '%s' and " "expiration of %s seconds for %s", + key, + expiration, + user_msg, + ) - data = json.dumps( - dict(session), cls=json_encoding.SessionEncoder - ).encode('utf-8') + data = json.dumps(dict(session), cls=json_encoding.SessionEncoder).encode( + "utf-8" + ) if self.redis.set(key, data): return self.redis.expire(key, expiration) def delete(self, session): key = self.build_key(session.sid) - _logger.debug('deleting session with key %s', key) + _logger.debug("deleting session with key %s", key) return self.redis.delete(key) def get(self, sid): if not self.is_valid_key(sid): - _logger.debug("session with invalid sid '%s' has been asked, " - "returning a new one", sid) + _logger.debug( + "session with invalid sid '%s' has been asked, " "returning a new one", + sid, + ) return self.new() key = self.build_key(sid) saved = self.redis.get(key) if not saved: - _logger.debug("session with non-existent key '%s' has been asked, " - "returning a new one", key) + _logger.debug( + "session with non-existent key '%s' has been asked, " + "returning a new one", + key, + ) return self.new() try: - data = json.loads( - saved.decode('utf-8'), cls=json_encoding.SessionDecoder - ) + data = json.loads(saved.decode("utf-8"), cls=json_encoding.SessionDecoder) except ValueError: - _logger.debug("session for key '%s' has been asked but its json " - "content could not be read, it has been reset", key) + _logger.debug( + "session for key '%s' has been asked but its json " + "content could not be read, it has been reset", + key, + ) data = {} return self.session_class(data, sid, False) def list(self): - keys = self.redis.keys('%s*' % self.prefix) + keys = self.redis.keys("%s*" % self.prefix) _logger.debug("a listing redis keys has been called") - return [key[len(self.prefix):] for key in keys] + return [key[len(self.prefix) :] for key in keys] diff --git a/test_base_fileurl_field/__manifest__.py b/test_base_fileurl_field/__manifest__.py index 5aeaee07..9d084dfa 100644 --- a/test_base_fileurl_field/__manifest__.py +++ b/test_base_fileurl_field/__manifest__.py @@ -6,10 +6,9 @@ "version": "12.0.1.0.0", "category": "Tests", "author": "Camptocamp,Odoo Community Association (OCA)", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "license": "AGPL-3", - "depends": [ - "base_fileurl_field" - ], + "depends": ["base_fileurl_field"], "data": [ "views/res_partner.xml", "views/res_users.xml", diff --git a/test_base_fileurl_field/data/sample.txt b/test_base_fileurl_field/data/sample.txt index 5251e0f1..8a03e0e5 100644 --- a/test_base_fileurl_field/data/sample.txt +++ b/test_base_fileurl_field/data/sample.txt @@ -1 +1 @@ -This is a simple text file. \ No newline at end of file +This is a simple text file. diff --git a/test_base_fileurl_field/models/res_partner.py b/test_base_fileurl_field/models/res_partner.py index 359843c9..48ca0a44 100644 --- a/test_base_fileurl_field/models/res_partner.py +++ b/test_base_fileurl_field/models/res_partner.py @@ -1,44 +1,46 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError class ResPartner(models.Model): - _inherit = 'res.partner' + _inherit = "res.partner" name = fields.Char() url_file = fields.FileURL( - storage_location='s3', - filename='url_file_fname', - storage_path='partner' + storage_location="s3", filename="url_file_fname", storage_path="partner" ) url_file_fname = fields.Char() url_image = fields.FileURL( - storage_location='s3', - filename='url_image_fname', - storage_path='partner_image', + storage_location="s3", + filename="url_image_fname", + storage_path="partner_image", ) url_image_fname = fields.Char() - @api.constrains('url_file', 'url_file_fname') + @api.constrains("url_file", "url_file_fname") def _check_url_file_fname(self): - rec = self.search([('url_file_fname', '=', self.url_file_fname)]) + rec = self.search([("url_file_fname", "=", self.url_file_fname)]) if len(rec) > 1: - raise ValidationError(_( - "This file name is already used on an existing record. " - "Please use another file name or delete the url_file on :\n" - "Model: %s Id: %s" % (self._name, rec.id) - )) + raise ValidationError( + _( + "This file name is already used on an existing record. " + "Please use another file name or delete the url_file on :\n" + "Model: %s Id: %s" % (self._name, rec.id) + ) + ) - @api.constrains('url_image', 'url_image_fname') + @api.constrains("url_image", "url_image_fname") def _check_url_image_fname(self): - rec = self.search([('url_image_fname', '=', self.url_image_fname)]) + rec = self.search([("url_image_fname", "=", self.url_image_fname)]) if len(rec) > 1: - raise ValidationError(_( - "This file name is already used on an existing record. " - "Please use another file name or delete the url_image on :\n" - "Model: %s Id: %s" % (self._name, rec.id) - )) + raise ValidationError( + _( + "This file name is already used on an existing record. " + "Please use another file name or delete the url_image on :\n" + "Model: %s Id: %s" % (self._name, rec.id) + ) + ) diff --git a/test_base_fileurl_field/models/res_users.py b/test_base_fileurl_field/models/res_users.py index da0b80b2..c8bc3240 100644 --- a/test_base_fileurl_field/models/res_users.py +++ b/test_base_fileurl_field/models/res_users.py @@ -1,11 +1,11 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models, fields +from odoo import fields, models class ResUsers(models.Model): - _inherit = 'res.users' + _inherit = "res.users" - partner_url_file = fields.FileURL(related='partner_id.url_file') - partner_url_file_fname = fields.Char(related='partner_id.url_file_fname') + partner_url_file = fields.FileURL(related="partner_id.url_file") + partner_url_file_fname = fields.Char(related="partner_id.url_file_fname") diff --git a/test_base_fileurl_field/tests/ir_attachment.py b/test_base_fileurl_field/tests/ir_attachment.py index d07017db..88035717 100644 --- a/test_base_fileurl_field/tests/ir_attachment.py +++ b/test_base_fileurl_field/tests/ir_attachment.py @@ -3,7 +3,7 @@ import logging -from odoo import models, api +from odoo import api, models _logger = logging.getLogger(__name__) @@ -14,23 +14,23 @@ class IrAttachment(models.Model): _inherit = "ir.attachment" def _get_stores(self): - l = ['s3'] + l = ["s3"] l += super(IrAttachment, self)._get_stores() return l @api.model def _store_file_read(self, fname, bin_size=False): - if fname.startswith('s3://'): + if fname.startswith("s3://"): return FAKE_S3_BUCKET.get(fname) else: return super(IrAttachment, self)._store_file_read(fname, bin_size) @api.model def _store_file_write(self, key, bin_data): - location = self.env.context.get('storage_location') or self._storage() - if location == 's3': + location = self.env.context.get("storage_location") or self._storage() + if location == "s3": FAKE_S3_BUCKET[key] = bin_data - filename = 's3://fake_bucket/%s' % key + filename = "s3://fake_bucket/%s" % key else: _super = super(IrAttachment, self) filename = _super._store_file_write(key, bin_data) @@ -38,7 +38,7 @@ def _store_file_write(self, key, bin_data): @api.model def _store_file_delete(self, fname): - if fname.startswith('s3://'): + if fname.startswith("s3://"): FAKE_S3_BUCKET.pop(fname) else: super(IrAttachment, self)._store_file_delete(fname) diff --git a/test_base_fileurl_field/tests/test_fileurl_fields.py b/test_base_fileurl_field/tests/test_fileurl_fields.py index c56bbe15..8be2bec3 100644 --- a/test_base_fileurl_field/tests/test_fileurl_fields.py +++ b/test_base_fileurl_field/tests/test_fileurl_fields.py @@ -2,38 +2,41 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import base64 -from odoo.tests import TransactionCase -from odoo.modules.module import get_module_resource from odoo.exceptions import ValidationError +from odoo.modules.module import get_module_resource +from odoo.tests import TransactionCase class TestFileUrlFields(TransactionCase): - def test_fileurl_fields(self): - file_path = get_module_resource('test_base_fileurl_field', 'data', - 'sample.txt') - image_path = get_module_resource('test_base_fileurl_field', 'data', - 'pattern.png') - partner = self.env.ref('base.main_partner') - with open(file_path, 'rb') as f: - with open(image_path, 'rb') as i: - partner.write({ - 'url_file': base64.b64encode(f.read()), - 'url_file_fname': 'sample.txt', - 'url_image': base64.b64encode(i.read()), - 'url_image_fname': 'pattern.png', - }) + file_path = get_module_resource("test_base_fileurl_field", "data", "sample.txt") + image_path = get_module_resource( + "test_base_fileurl_field", "data", "pattern.png" + ) + partner = self.env.ref("base.main_partner") + with open(file_path, "rb") as f: + with open(image_path, "rb") as i: + partner.write( + { + "url_file": base64.b64encode(f.read()), + "url_file_fname": "sample.txt", + "url_image": base64.b64encode(i.read()), + "url_image_fname": "pattern.png", + } + ) - with open(file_path, 'rb') as f: + with open(file_path, "rb") as f: self.assertEqual(base64.decodebytes(partner.url_file), f.read()) - with open(image_path, 'rb') as i: + with open(image_path, "rb") as i: self.assertEqual(base64.decodebytes(partner.url_image), i.read()) - partner2 = self.env.ref('base.partner_admin') - with open(file_path, 'rb') as f: + partner2 = self.env.ref("base.partner_admin") + with open(file_path, "rb") as f: with self.assertRaises(ValidationError): - partner2.write({ - 'url_file': base64.b64encode(f.read()), - 'url_file_fname': 'sample.txt', - }) + partner2.write( + { + "url_file": base64.b64encode(f.read()), + "url_file_fname": "sample.txt", + } + ) diff --git a/test_base_fileurl_field/views/res_partner.xml b/test_base_fileurl_field/views/res_partner.xml index 08c33e44..f3367e99 100644 --- a/test_base_fileurl_field/views/res_partner.xml +++ b/test_base_fileurl_field/views/res_partner.xml @@ -1,4 +1,4 @@ - + res.partner.form.inherit @@ -9,11 +9,15 @@ - + - - + + diff --git a/test_base_fileurl_field/views/res_users.xml b/test_base_fileurl_field/views/res_users.xml index ffb56753..cbfdc722 100644 --- a/test_base_fileurl_field/views/res_users.xml +++ b/test_base_fileurl_field/views/res_users.xml @@ -1,4 +1,4 @@ - + res.users.form.inherit @@ -9,7 +9,7 @@ - + From 483e328ab238a0d14ae3fa485991ac0a52d7e40e Mon Sep 17 00:00:00 2001 From: Yannick Payot Date: Wed, 24 May 2023 17:57:28 +0200 Subject: [PATCH 2/2] Fix CI issue with cryptography version --- test-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 test-requirements.txt diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..a6a96764 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,2 @@ +# avoid openssl breaking change https://github.com/odoo/odoo/issues/99809 +cryptography<38