diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 00000000..f23d2e3a --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,25 @@ +# 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: 16.0 +org_name: Camptocamp +org_slug: camptocamp +rebel_module_groups: +- 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..f4f16d33 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,35 @@ +name: pre-commit + +on: + pull_request: + branches: + - "16.0*" + push: + branches: + - "16.0" + - "16.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..d2e29c9d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,82 @@ +name: tests + +on: + pull_request: + branches: + - "16.0*" + push: + branches: + - "16.0" + - "16.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.10-odoo16.0:latest + include: "attachment_azure,cloud_platform_azure" + makepot: "false" + name: test azure with Odoo + - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest + include: "attachment_azure,cloud_platform_azure" + name: test azure with OCA + - container: ghcr.io/oca/oca-ci/py3.10-odoo16.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.10-ocb16.0:latest + exclude: "attachment_s3,cloud_platform_exoscale,attachment_swift,cloud_platform_ovh,attachment_azure,cloud_platform_azure" + name: test others with OCB + services: + postgres: + image: postgres:12.0 + 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..18a030da --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,140 @@ +exclude: | + (?x) + # NOT INSTALLABLE ADDONS + ^attachment_s3/| + ^attachment_swift/| + ^base_fileurl_field/| + ^cloud_platform_exoscale/| + ^cloud_platform_ovh/| + ^monitoring_log_requests/| + ^monitoring_statsd/| + ^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: "16.17.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: 4cd2b852214dead80822e93e6749b16f2785b2fe + 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.6.1 + 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.8.0 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + name: prettier (with plugin-xml) + additional_dependencies: + - "prettier@2.7.1" + - "@prettier/plugin-xml@2.2.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: v8.24.0 + hooks: + - id: eslint + verbose: true + args: + - --color + - --fix + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + 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.38.2 + 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..54a5e915 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,123 @@ + + +[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=16.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, + external-request-timeout, + # 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..fedc3ea6 --- /dev/null +++ b/.pylintrc-mandatory @@ -0,0 +1,98 @@ + +[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=16.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, + external-request-timeout + +[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 8e1aa7ef..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.8" - -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="16.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..4b81d686 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -[![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=16.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/pre-commit.yml?query=branch%3A16.0) +[![Build Status](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml/badge.svg?branch=16.0)](https://github.com/camptocamp/odoo-cloud-platform/actions/workflows/test.yml?query=branch%3A16.0) +[![codecov](https://codecov.io/gh/camptocamp/odoo-cloud-platform/branch/16.0/graph/badge.svg)](https://codecov.io/gh/camptocamp/odoo-cloud-platform) + + + # Odoo Cloud Addons @@ -167,3 +174,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 25388175..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,15 +105,13 @@ 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) @@ -143,7 +139,7 @@ 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 @@ -181,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) diff --git a/attachment_azure/models/ir_binary.py b/attachment_azure/models/ir_binary.py index 7c99ffac..f08fc874 100644 --- a/attachment_azure/models/ir_binary.py +++ b/attachment_azure/models/ir_binary.py @@ -60,6 +60,6 @@ def _record_to_stream(self, record, field_name): from odoo.addons import documents documents.models.ir_binary.IrBinary._record_to_stream = IrBinary._record_to_stream -except ImportError: +except ImportError: # pylint: disable=except-pass # document enterprise module if not installed, we just ignore pass 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 fcdc6cb8..7958e620 100644 --- a/attachment_s3/__manifest__.py +++ b/attachment_s3/__manifest__.py @@ -13,7 +13,7 @@ "external_dependencies": { "python": ["boto3"], }, - "website": "https://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": False, } 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 7991433b..d0c2ed9c 100644 --- a/attachment_swift/__manifest__.py +++ b/attachment_swift/__manifest__.py @@ -17,7 +17,7 @@ "keystoneauth1", ], }, - "website": "https://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": False, } 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 39cc63aa..c82d990a 100644 --- a/base_attachment_object_storage/__manifest__.py +++ b/base_attachment_object_storage/__manifest__.py @@ -10,7 +10,7 @@ "license": "AGPL-3", "category": "Knowledge Management", "depends": ["base"], - "website": "http://www.camptocamp.com", + "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 a4e3ec9d..e8b17f54 100644 --- a/base_attachment_object_storage/models/ir_attachment.py +++ b/base_attachment_object_storage/models/ir_attachment.py @@ -5,16 +5,16 @@ import logging import os import time -from .strtobool import strtobool +from contextlib import closing, contextmanager 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 +from .strtobool import strtobool _logger = logging.getLogger(__name__) @@ -242,7 +242,7 @@ def _file_delete(self, fname): 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): @@ -395,7 +395,8 @@ def _force_storage_to_object_storage(self, new_cr=False): # 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 + # https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347 # noqa: B950 + domain = [ "!", ("store_fname", "=like", "{}://%".format(storage)), diff --git a/base_attachment_object_storage/models/strtobool.py b/base_attachment_object_storage/models/strtobool.py index 44d1eb2c..12f4b828 100644 --- a/base_attachment_object_storage/models/strtobool.py +++ b/base_attachment_object_storage/models/strtobool.py @@ -1,21 +1,21 @@ _MAP = { - 'y': True, - 'yes': True, - 't': True, - 'true': True, - 'on': True, - '1': True, - 'n': False, - 'no': False, - 'f': False, - 'false': False, - 'off': False, - '0': False + "y": True, + "yes": True, + "t": True, + "true": True, + "on": True, + "1": True, + "n": False, + "no": False, + "f": False, + "false": False, + "off": False, + "0": False, } def strtobool(value): try: return _MAP[str(value).lower()] - except KeyError: - raise ValueError('"{}" is not a valid bool value'.format(value)) + except KeyError as error: + raise ValueError('"{}" is not a valid bool value'.format(value)) from error 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 8cd5907e..1570e60d 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 0c5bb58a..fc63b366 100644 --- a/cloud_platform/__manifest__.py +++ b/cloud_platform/__manifest__.py @@ -15,7 +15,7 @@ "logging_json", "server_environment", # OCA/server-tools ], - "website": "https://www.camptocamp.com", + "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 65a4d112..e55cfa51 100644 --- a/cloud_platform/models/cloud_platform.py +++ b/cloud_platform/models/cloud_platform.py @@ -4,46 +4,39 @@ import logging import os import re - from collections import namedtuple -from .strtobool import strtobool from odoo import api, models from odoo.tools.config import config +from .strtobool import strtobool _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 +46,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 +82,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/models/strtobool.py b/cloud_platform/models/strtobool.py index 44d1eb2c..12f4b828 100644 --- a/cloud_platform/models/strtobool.py +++ b/cloud_platform/models/strtobool.py @@ -1,21 +1,21 @@ _MAP = { - 'y': True, - 'yes': True, - 't': True, - 'true': True, - 'on': True, - '1': True, - 'n': False, - 'no': False, - 'f': False, - 'false': False, - 'off': False, - '0': False + "y": True, + "yes": True, + "t": True, + "true": True, + "on": True, + "1": True, + "n": False, + "no": False, + "f": False, + "false": False, + "off": False, + "0": False, } def strtobool(value): try: return _MAP[str(value).lower()] - except KeyError: - raise ValueError('"{}" is not a valid bool value'.format(value)) + except KeyError as error: + raise ValueError('"{}" is not a valid bool value'.format(value)) from error 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 30dab452..b1179587 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 55f298b1..45ead050 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": False, } 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 6467f581..8f22580e 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": False, } 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/kwkhtmltopdf_assets/__init__.py b/kwkhtmltopdf_assets/__init__.py index a9e33722..0650744f 100644 --- a/kwkhtmltopdf_assets/__init__.py +++ b/kwkhtmltopdf_assets/__init__.py @@ -1,2 +1 @@ - from . import models diff --git a/kwkhtmltopdf_assets/__manifest__.py b/kwkhtmltopdf_assets/__manifest__.py index 02b1ab9d..c0d30249 100644 --- a/kwkhtmltopdf_assets/__manifest__.py +++ b/kwkhtmltopdf_assets/__manifest__.py @@ -11,7 +11,7 @@ "depends": [ "base", ], - "website": "http://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": True, } diff --git a/kwkhtmltopdf_assets/models/__init__.py b/kwkhtmltopdf_assets/models/__init__.py index 0e71e216..33ac73ee 100644 --- a/kwkhtmltopdf_assets/models/__init__.py +++ b/kwkhtmltopdf_assets/models/__init__.py @@ -1,2 +1 @@ - from . import ir_qweb diff --git a/kwkhtmltopdf_assets/models/ir_qweb.py b/kwkhtmltopdf_assets/models/ir_qweb.py index 354fe300..9025b96c 100644 --- a/kwkhtmltopdf_assets/models/ir_qweb.py +++ b/kwkhtmltopdf_assets/models/ir_qweb.py @@ -1,8 +1,8 @@ # Copyright 2016-2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo.tools import config from odoo import models +from odoo.tools import config class IrQweb(models.AbstractModel): 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 c94984e8..42475b8b 100644 --- a/logging_json/__manifest__.py +++ b/logging_json/__manifest__.py @@ -13,7 +13,7 @@ "external_dependencies": { "python": ["python-json-logger"], }, - "website": "http://www.camptocamp.com", + "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 60313b38..cc7efcb4 100644 --- a/logging_json/json_log.py +++ b/logging_json/json_log.py @@ -6,10 +6,10 @@ import threading import uuid -from .strtobool import strtobool - from odoo import http +from .strtobool import strtobool + _logger = logging.getLogger(__name__) try: diff --git a/logging_json/strtobool.py b/logging_json/strtobool.py index 44d1eb2c..12f4b828 100644 --- a/logging_json/strtobool.py +++ b/logging_json/strtobool.py @@ -1,21 +1,21 @@ _MAP = { - 'y': True, - 'yes': True, - 't': True, - 'true': True, - 'on': True, - '1': True, - 'n': False, - 'no': False, - 'f': False, - 'false': False, - 'off': False, - '0': False + "y": True, + "yes": True, + "t": True, + "true": True, + "on": True, + "1": True, + "n": False, + "no": False, + "f": False, + "false": False, + "off": False, + "0": False, } def strtobool(value): try: return _MAP[str(value).lower()] - except KeyError: - raise ValueError('"{}" is not a valid bool value'.format(value)) + except KeyError as error: + raise ValueError('"{}" is not a valid bool value'.format(value)) from error 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 bbc6cea3..687c37e0 100644 --- a/monitoring_log_requests/__manifest__.py +++ b/monitoring_log_requests/__manifest__.py @@ -9,7 +9,7 @@ "license": "AGPL-3", "category": "category", "depends": ["base", "web"], - "website": "http://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": False, } 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 559115da..3df49231 100644 --- a/monitoring_log_requests/models/ir_http.py +++ b/monitoring_log_requests/models/ir_http.py @@ -9,7 +9,6 @@ from odoo.http import request as http_request from odoo.tools.config import config - _logger = logging.getLogger("monitoring.http.requests") diff --git a/monitoring_prometheus/__manifest__.py b/monitoring_prometheus/__manifest__.py index 6809e1b1..5745fa30 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 050ca5bb..b2316c81 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 0d811c52..d841d58b 100644 --- a/monitoring_statsd/__manifest__.py +++ b/monitoring_statsd/__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": ["statsd"], 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 db430d9f..7b7d47a0 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, endpoint): if not statsd: return super()._dispatch(endpoint) - 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(endpoint) - 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(endpoint) diff --git a/monitoring_statsd/models/strtobool.py b/monitoring_statsd/models/strtobool.py index 44d1eb2c..6a6881c8 100644 --- a/monitoring_statsd/models/strtobool.py +++ b/monitoring_statsd/models/strtobool.py @@ -1,16 +1,16 @@ _MAP = { - 'y': True, - 'yes': True, - 't': True, - 'true': True, - 'on': True, - '1': True, - 'n': False, - 'no': False, - 'f': False, - 'false': False, - 'off': False, - '0': False + "y": True, + "yes": True, + "t": True, + "true": True, + "on": True, + "1": True, + "n": False, + "no": False, + "f": False, + "false": False, + "off": False, + "0": False, } diff --git a/monitoring_status/__manifest__.py b/monitoring_status/__manifest__.py index 9a00dfb2..37dad5c6 100644 --- a/monitoring_status/__manifest__.py +++ b/monitoring_status/__manifest__.py @@ -9,7 +9,7 @@ "license": "AGPL-3", "category": "category", "depends": ["base", "web"], - "website": "http://www.camptocamp.com", + "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 6a7240a0..e4acaf78 100644 --- a/session_redis/__manifest__.py +++ b/session_redis/__manifest__.py @@ -13,7 +13,7 @@ "external_dependencies": { "python": ["redis"], }, - "website": "http://www.camptocamp.com", + "website": "https://github.com/camptocamp/odoo-cloud-platform", "data": [], "installable": True, } diff --git a/session_redis/http.py b/session_redis/http.py index ad6e3949..64595296 100644 --- a/session_redis/http.py +++ b/session_redis/http.py @@ -4,13 +4,12 @@ import logging import os -from .strtobool import strtobool - from odoo import http from odoo.tools import config from odoo.tools.func import lazy_property from .session import RedisSessionStore +from .strtobool import strtobool _logger = logging.getLogger(__name__) @@ -52,10 +51,13 @@ def session_store(self): 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.Session) + return RedisSessionStore( + redis=redis_client, + prefix=prefix, + expiration=expiration, + anon_expiration=anon_expiration, + session_class=http.Session, + ) def purge_fs_sessions(path): @@ -64,7 +66,7 @@ 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")): @@ -77,8 +79,12 @@ def purge_fs_sessions(path): 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.Application.session_store = session_store # clean the existing sessions on the file system purge_fs_sessions(config.session_dir) 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 57ab9e02..9854268c 100644 --- a/session_redis/session.py +++ b/session_redis/session.py @@ -18,10 +18,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: @@ -32,14 +38,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) @@ -52,51 +56,59 @@ 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] def rotate(self, session, env): self.delete(session) @@ -106,7 +118,7 @@ def rotate(self, session, env): self.save(session) def vacuum(self): - """ Do not garbage collect the sessions + """Do not garbage collect the sessions Redis keys are automatically cleaned at the end of their expiration. diff --git a/session_redis/strtobool.py b/session_redis/strtobool.py index 44d1eb2c..12f4b828 100644 --- a/session_redis/strtobool.py +++ b/session_redis/strtobool.py @@ -1,21 +1,21 @@ _MAP = { - 'y': True, - 'yes': True, - 't': True, - 'true': True, - 'on': True, - '1': True, - 'n': False, - 'no': False, - 'f': False, - 'false': False, - 'off': False, - '0': False + "y": True, + "yes": True, + "t": True, + "true": True, + "on": True, + "1": True, + "n": False, + "no": False, + "f": False, + "false": False, + "off": False, + "0": False, } def strtobool(value): try: return _MAP[str(value).lower()] - except KeyError: - raise ValueError('"{}" is not a valid bool value'.format(value)) + except KeyError as error: + raise ValueError('"{}" is not a valid bool value'.format(value)) from error 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 @@ - +