├── .github └── workflows │ ├── dependency-analysis.yml │ ├── docs.yml │ ├── lint-pr-title.yml │ ├── pypi.yml │ ├── python-package.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .release-it.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── conftest.py ├── crowdin_api ├── __init__.py ├── api_resources │ ├── __init__.py │ ├── abstract │ │ ├── __init__.py │ │ ├── resources.py │ │ └── tests │ │ │ └── test_resources.py │ ├── ai │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_ai_resources.py │ │ └── types.py │ ├── application │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_applications_resources.py │ │ └── types.py │ ├── branches │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_branches.py │ │ └── types.py │ ├── bundles │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_bundles_resources.py │ │ └── types.py │ ├── clients │ │ ├── __init__.py │ │ └── resource.py │ ├── dictionaries │ │ ├── __init__.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_dictionaries_resources.py │ │ └── types.py │ ├── distributions │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_distributions_resources.py │ │ └── types.py │ ├── enums.py │ ├── fields │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_fields_resources.py │ │ └── types.py │ ├── glossaries │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_glossaries_resources.py │ │ └── types.py │ ├── groups │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_groups_resources.py │ │ └── types.py │ ├── labels │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_labels_resources.py │ │ └── types.py │ ├── languages │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_languages_resources.py │ │ └── types.py │ ├── machine_translation_engines │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ └── tests │ │ │ └── test_machine_translation_engines_resources.py │ ├── notifications │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_notification_resources.py │ │ └── types.py │ ├── projects │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_projects_resources.py │ │ └── types.py │ ├── reports │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── requests │ │ │ ├── __init__.py │ │ │ ├── cost_estimation_post_editing.py │ │ │ ├── group_translation_costs_post_editing.py │ │ │ └── translation_costs_post_editing.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_reports_resources.py │ │ └── types.py │ ├── screenshots │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_screenshots_resources.py │ │ └── types.py │ ├── security_logs │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ └── tests │ │ │ └── test_security_logs_resource.py │ ├── source_files │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_source_files_resources.py │ │ └── types.py │ ├── source_strings │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_source_strings_resources.py │ │ └── types.py │ ├── storages │ │ ├── __init__.py │ │ ├── resource.py │ │ └── tests │ │ │ └── test_storages_resources.py │ ├── string_comments │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_string_comments_resources.py │ │ └── types.py │ ├── string_translations │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_string_translations_resources.py │ │ └── types.py │ ├── tasks │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_tasks_resources.py │ │ └── types.py │ ├── teams │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_teams_resources.py │ │ └── types.py │ ├── translation_memory │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_translation_memory_resources.py │ │ └── types.py │ ├── translation_status │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ └── tests │ │ │ └── test_translation_status_resources.py │ ├── translations │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_translations_resources.py │ │ └── types.py │ ├── users │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_users_resources.py │ │ └── types.py │ ├── vendors │ │ ├── __init__.py │ │ ├── resource.py │ │ └── tests │ │ │ └── test_vendos_resources.py │ ├── webhooks │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── organization │ │ │ ├── __init__.py │ │ │ ├── enums.py │ │ │ ├── resource.py │ │ │ ├── tests │ │ │ │ └── test_organization_webhooks_resources.py │ │ │ └── types.py │ │ ├── resource.py │ │ ├── tests │ │ │ └── test_webhooks_resources.py │ │ └── types.py │ └── workflows │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── resource.py │ │ └── tests │ │ └── test_workflows_resources.py ├── client.py ├── enums.py ├── exceptions.py ├── fixtures.py ├── parser.py ├── requester.py ├── sorting.py ├── status.py ├── tests │ ├── test_client.py │ ├── test_client_methods.py │ ├── test_exceptions.py │ ├── test_parser.py │ ├── test_requester.py │ └── test_sorting.py ├── typing.py └── utils.py ├── requirements ├── requirements-dev.txt ├── requirements-doc.txt └── requirements.txt ├── setup.cfg └── setup.py /.github/workflows/dependency-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | schedule: 8 | - cron: '0 0 * * MON' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | dependency-analysis: 13 | uses: crowdin/.github/.github/workflows/dependency-analysis.yml@main 14 | secrets: 15 | FOSSA_API_KEY: ${{ secrets.FOSSA_API_KEY }} 16 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | repository_dispatch: 7 | types: [publish] 8 | 9 | jobs: 10 | docs: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r ./requirements/requirements-doc.txt 24 | 25 | - name: Generate the docs locally in CI 26 | run: pdoc ./crowdin_api/ --html --config show_source_code=False --force --output-dir docs 27 | 28 | - name: Deploy 🚀 29 | uses: JamesIves/github-pages-deploy-action@v4 30 | with: 31 | branch: gh-pages 32 | folder: docs/crowdin_api 33 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: lint-pr-title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: amannn/action-semantic-pull-request@v5 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Deployment 2 | 3 | on: 4 | release: 5 | types: [published] 6 | repository_dispatch: 7 | types: [publish] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 3.8 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install pypa/build 21 | run: >- 22 | python -m 23 | pip install --user setuptools==66.0.0 wheel==0.38.4 twine==4.0.2 24 | build 25 | --user 26 | 27 | - name: Build a binary wheel and a source tarball 28 | run: >- 29 | python -m 30 | build 31 | --sdist 32 | --wheel 33 | --outdir dist/ 34 | 35 | - name: Publish package 36 | uses: pypa/gh-action-pypi-publish@release/v1 37 | with: 38 | user: __token__ 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12' ] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install -r ./requirements/requirements-dev.txt 32 | 33 | - name: Lint with flake8 34 | run: flake8 . --count --show-source --statistics 35 | 36 | - name: Test with pytest 37 | run: pytest 38 | 39 | - name: Comment coverage 40 | uses: codecov/codecov-action@v4 41 | with: 42 | token: ${{ secrets.CODECOV_TOKEN }} 43 | fail_ci_if_error: true 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | run-name: Release ${{ github.event.inputs.version }} version 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | type: choice 9 | description: Version 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | jobs: 16 | version: 17 | permissions: 18 | contents: write 19 | uses: crowdin/.github/.github/workflows/bump-version.yml@main 20 | 21 | publish: 22 | runs-on: ubuntu-latest 23 | needs: version 24 | permissions: 25 | contents: write 26 | steps: 27 | - uses: peter-evans/repository-dispatch@v3 28 | with: 29 | event-type: publish 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | 128 | ### JetBrains template 129 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 130 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 131 | .idea 132 | # User-specific stuff 133 | .idea/**/workspace.xml 134 | .idea/**/tasks.xml 135 | .idea/**/usage.statistics.xml 136 | .idea/**/dictionaries 137 | .idea/**/shelf 138 | 139 | # Generated files 140 | .idea/**/contentModel.xml 141 | 142 | # Sensitive or high-churn files 143 | .idea/**/dataSources/ 144 | .idea/**/dataSources.ids 145 | .idea/**/dataSources.local.xml 146 | .idea/**/sqlDataSources.xml 147 | .idea/**/dynamic.xml 148 | .idea/**/uiDesigner.xml 149 | .idea/**/dbnavigator.xml 150 | 151 | # Gradle 152 | .idea/**/gradle.xml 153 | .idea/**/libraries 154 | 155 | # Gradle and Maven with auto-import 156 | # When using Gradle or Maven with auto-import, you should exclude module files, 157 | # since they will be recreated, and may cause churn. Uncomment if using 158 | # auto-import. 159 | # .idea/modules.xml 160 | # .idea/*.iml 161 | # .idea/modules 162 | # *.iml 163 | # *.ipr 164 | 165 | # CMake 166 | cmake-build-*/ 167 | 168 | # Mongo Explorer plugin 169 | .idea/**/mongoSettings.xml 170 | 171 | # File-based project format 172 | *.iws 173 | 174 | # IntelliJ 175 | out/ 176 | 177 | # mpeltonen/sbt-idea plugin 178 | .idea_modules/ 179 | 180 | # JIRA plugin 181 | atlassian-ide-plugin.xml 182 | 183 | # Cursive Clojure plugin 184 | .idea/replstate.xml 185 | 186 | # Crashlytics plugin (for Android Studio and IntelliJ) 187 | com_crashlytics_export_strings.xml 188 | crashlytics.properties 189 | crashlytics-build.properties 190 | fabric.properties 191 | 192 | # Editor-based Rest Client 193 | .idea/httpRequests 194 | 195 | # Android studio 3.1+ serialized cache file 196 | .idea/caches/build_file_checksums.ser 197 | 198 | #vscode 199 | .vscode 200 | 201 | .local/ 202 | demo/ 203 | example/ 204 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/mirrors-isort 4 | rev: v5.10.1 5 | hooks: 6 | - id: isort 7 | - repo: https://github.com/psf/black-pre-commit-mirror 8 | rev: 23.9.1 9 | hooks: 10 | - id: black 11 | args: ["-l 100"] 12 | language_version: python3.9 13 | - repo: https://github.com/pycqa/flake8 14 | rev: 6.1.0 15 | hooks: 16 | - id: flake8 17 | - repo: https://github.com/yunojuno/pre-commit-xenon 18 | rev: v0.1 19 | hooks: 20 | - id: xenon 21 | args: ["-e=*tests/*,venv/*,tasks.py", "--max-average=A", "--max-modules=A", "--max-absolute=C"] 22 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "push": true, 4 | "commit": true, 5 | "commitMessage": "chore: version ${version} [skip ci]", 6 | "requireBranch": "main", 7 | "tag": true 8 | }, 9 | "github": { 10 | "release": true, 11 | "autoGenerate": true, 12 | "releaseName": "${version}" 13 | }, 14 | "plugins": { 15 | "@j-ulrich/release-it-regex-bumper": { 16 | "in": "crowdin_api/__init__.py", 17 | "out": [ 18 | { 19 | "file": "crowdin_api/__init__.py", 20 | "search": "__version__ = \"\\d+\\.\\d+\\.\\d+\"", 21 | "replace": "__version__ = \"{{versionWithoutPrerelease}}\"" 22 | } 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at support@crowdin.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | :tada: First off, thanks for taking the time to contribute! :tada: 4 | 5 | The Crowdin API client provides methods that essentially call Crowdin's APIs. This makes it much easier for other developers to make calls to Crowdin's APIs, as the client abstracts a lot of the work required. In short, the API client provides a lightweight interface for making API requests to Crowdin. 6 | 7 | The following is a set of guidelines for contributing to Crowdin Python Client. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 8 | 9 | This project and everyone participating in it are governed by the [Code of Conduct](/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. 10 | 11 | ## How can I contribute? 12 | 13 | ### Star this repo 14 | 15 | It's quick and goes a long way! :stars: 16 | 17 | ### Reporting Bugs 18 | 19 | This section guides you through submitting a bug report for Crowdin Python Client. Following these guidelines helps maintainers, and the community understand your report :pencil:, reproduce the behavior :computer:, and find related reports :mag_right:. 20 | 21 | When you are creating a bug report, please include as many details as possible. 22 | 23 | #### How Do I Submit a Bug Report? 24 | 25 | Bugs are tracked as [GitHub issues](https://github.com/crowdin/crowdin-api-client-python/issues/). 26 | 27 | Explain the problem and include additional details to help reproduce the problem: 28 | 29 | * **Use a clear and descriptive title** for the issue to identify the problem. 30 | * **Describe the exact steps which reproduce the problem** in as many details as possible. Don't just say what you did, but explain how you did it. 31 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 32 | * **Explain which behavior you expected to see instead and why.** 33 | 34 | Include details about your environment. 35 | 36 | ### Suggesting Enhancements 37 | 38 | This section guides you through submitting an enhancement suggestion for Crowdin Python Client. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. 39 | 40 | When you are creating an enhancement suggestion, please include as many details as possible. 41 | 42 | #### How Do I Submit an Enhancement Suggestion? 43 | 44 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/crowdin/crowdin-api-client-python/issues/). 45 | 46 | Create an issue on that repository and provide the following information: 47 | 48 | * **Use a clear and descriptive title** for the issue to identify the suggestion. 49 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible. 50 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 51 | * **Explain why this enhancement would be useful** to most Python Client users. 52 | 53 | ### Your First Code Contribution 54 | 55 | Unsure where to begin contributing to Crowdin Python Client? You can start by looking through these `good-first-issue` and `help-wanted` issues: 56 | 57 | * [Good first issue](https://github.com/crowdin/crowdin-api-client-python/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) - issues which should only require a small amount of code, and a test or two. 58 | * [Help wanted](https://github.com/crowdin/crowdin-api-client-python/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) - issues which should be a bit more involved than `Good first issue` issues. 59 | 60 | #### Pull Request Checklist 61 | 62 | Before sending your pull requests, make sure you followed the list below: 63 | 64 | - Read these guidelines. 65 | - Read [Code of Conduct](/CODE_OF_CONDUCT.md). 66 | - Ensure that your code adheres to standard conventions, as used in the rest of the project. 67 | - Ensure that there are unit tests for your code. 68 | - Run unit tests. 69 | 70 | > **Note** 71 | > This project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for commit messages and PR titles. 72 | 73 | #### Docs 74 | 75 | ##### Generate 76 | 77 | To generate the docs run the following command: 78 | 79 | ```console 80 | pdoc ./crowdin_api/ --html --config show_source_code=False --force --output-dir docs 81 | ``` 82 | 83 | ##### Preview 84 | 85 | To preview the docs locally, run the following command: 86 | 87 | ```console 88 | pdoc --http : docs/* 89 | ``` 90 | 91 | Open `http://127.0.0.1:8080` in browser 92 | 93 | #### Philosophy of code contribution 94 | 95 | - Include unit tests when you contribute new features, as they help to a) prove that your code works correctly, and b) guard against future breaking changes to lower the maintenance cost. 96 | - Bug fixes also generally require unit tests, because the presence of bugs usually indicates insufficient test coverage. 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Crowdin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """This module is used to provide configuration, fixtures, and plugins for pytest. 2 | It may be also used for extending doctest's context: 3 | 1. https://docs.python.org/3/library/doctest.html 4 | 2. https://docs.pytest.org/en/latest/doctest.html 5 | """ 6 | 7 | pytest_plugins = ["crowdin_api.fixtures"] 8 | 9 | 10 | def pytest_configure(): 11 | """Configure settings.""" 12 | -------------------------------------------------------------------------------- /crowdin_api/__init__.py: -------------------------------------------------------------------------------- 1 | from crowdin_api.client import CrowdinClient 2 | 3 | __all__ = ["CrowdinClient"] 4 | __author__ = "Crowdin" 5 | __version__ = "1.24.0" 6 | 7 | __pdoc__ = { 8 | "tests": False, 9 | "fixtures": False 10 | } 11 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .ai.resource import AIResource, EnterpriseAIResource 2 | from .application.resource import ApplicationResource 3 | from .bundles.resource import BundlesResource 4 | from .dictionaries.resource import DictionariesResource 5 | from .distributions.resource import DistributionsResource 6 | from .fields.resource import FieldsResource 7 | from .glossaries.resource import GlossariesResource 8 | from .groups.resource import GroupsResource 9 | from .labels.resource import LabelsResource 10 | from .languages.resource import LanguagesResource 11 | from .machine_translation_engines.resource import MachineTranslationEnginesResource 12 | from .projects.resource import ProjectsResource 13 | from .reports.resource import ReportsResource, EnterpriseReportsResource 14 | from .screenshots.resource import ScreenshotsResource 15 | from .security_logs.resource import SecurityLogsResource 16 | from .source_files.resource import SourceFilesResource 17 | from .source_strings.resource import SourceStringsResource 18 | from .storages.resource import StoragesResource 19 | from .string_comments.resource import StringCommentsResource 20 | from .string_translations.resource import StringTranslationsResource 21 | from .tasks.resource import TasksResource, EnterpriseTasksResource 22 | from .teams.resource import TeamsResource 23 | from .translation_memory.resource import TranslationMemoryResource 24 | from .translation_status.resource import TranslationStatusResource 25 | from .translations.resource import TranslationsResource 26 | from .users.resource import UsersResource, EnterpriseUsersResource 27 | from .vendors.resource import VendorsResource 28 | from .webhooks.resource import WebhooksResource 29 | from .workflows.resource import WorkflowsResource 30 | 31 | __all__ = [ 32 | "AIResource", 33 | "EnterpriseAIResource", 34 | "ApplicationResource", 35 | "BundlesResource", 36 | "DictionariesResource", 37 | "DistributionsResource", 38 | "FieldsResource", 39 | "GlossariesResource", 40 | "GroupsResource", 41 | "LabelsResource", 42 | "LanguagesResource", 43 | "MachineTranslationEnginesResource", 44 | "ProjectsResource", 45 | "ReportsResource", 46 | "EnterpriseReportsResource", 47 | "ScreenshotsResource", 48 | "SecurityLogsResource", 49 | "SourceFilesResource", 50 | "SourceStringsResource", 51 | "StoragesResource", 52 | "StringCommentsResource", 53 | "StringTranslationsResource", 54 | "TasksResource", 55 | "EnterpriseTasksResource", 56 | "TeamsResource", 57 | "TranslationMemoryResource", 58 | "TranslationStatusResource", 59 | "TranslationsResource", 60 | "UsersResource", 61 | "EnterpriseUsersResource", 62 | "VendorsResource", 63 | "WebhooksResource", 64 | "WorkflowsResource", 65 | ] 66 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/abstract/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/abstract/resources.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta 2 | from typing import Optional 3 | 4 | from crowdin_api.requester import APIRequester 5 | 6 | 7 | class BaseResource(metaclass=ABCMeta): 8 | def __init__( 9 | self, requester: APIRequester, project_id: Optional[int] = None, page_size=25 10 | ): 11 | self.requester = requester 12 | self.project_id = project_id 13 | self.page_size = page_size 14 | self._flag_fetch_all = None 15 | self._max_limit = None 16 | 17 | def get_project_id(self): 18 | if self.project_id is None: 19 | raise ValueError("You must set project id for client") 20 | return self.project_id 21 | 22 | def _get_page_params(self, page: int): 23 | if page < 1: 24 | raise ValueError("The page number must be greater than or equal to 1.") 25 | 26 | return {"offset": max((page - 1) * self.page_size, 0), "limit": self.page_size} 27 | 28 | def get_page_params( 29 | self, 30 | page: Optional[int] = None, 31 | offset: Optional[int] = None, 32 | limit: Optional[int] = None, 33 | ): 34 | if page is not None and (offset is not None or limit is not None): 35 | raise ValueError("You must set page or offset and limit.") 36 | 37 | if page: 38 | return self._get_page_params(page=page) 39 | else: 40 | offset = offset or 0 41 | if offset < 0: 42 | raise ValueError("The offset must be greater than or equal to 0.") 43 | 44 | limit = limit or self.page_size 45 | 46 | if limit < 1: 47 | raise ValueError("The limit must be greater than or equal to 1.") 48 | 49 | return {"offset": offset, "limit": limit} 50 | 51 | def with_fetch_all(self, max_limit: Optional[int] = None): 52 | self._max_limit = max_limit 53 | self._flag_fetch_all = True 54 | return self 55 | 56 | def _get_entire_data(self, method: str, path: str, params: Optional[dict] = None): 57 | if not self._flag_fetch_all: 58 | return self.requester.request( 59 | method=method, 60 | path=path, 61 | params=params, 62 | ) 63 | 64 | contents = self._fetch_all( 65 | method=method, 66 | path=path, 67 | params=params, 68 | max_amount=self._max_limit 69 | ) 70 | self._flag_fetch_all = False 71 | self._max_limit = None 72 | return contents 73 | 74 | def _fetch_all( 75 | self, 76 | method: str, 77 | path: str, 78 | params: Optional[dict] = None, 79 | max_amount: Optional[int] = None 80 | ) -> list: 81 | limit = 500 82 | offset = 0 83 | join_data = [] 84 | if params is None: 85 | params = {} 86 | 87 | if max_amount and max_amount < limit: 88 | limit = max_amount 89 | 90 | while True: 91 | params.update({"limit": limit, "offset": offset}) 92 | 93 | content = self.requester.request(method=method, path=path, params=params) 94 | data = content.get("data", []) 95 | data and join_data.extend(data) 96 | 97 | if len(data) < limit or (max_amount and len(join_data) >= max_amount): 98 | break 99 | else: 100 | offset += limit 101 | 102 | if max_amount and max_amount < len(join_data) + limit: 103 | limit = max_amount - len(join_data) 104 | 105 | content["data"] = join_data 106 | return content 107 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/abstract/tests/test_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import Mock 3 | 4 | import pytest 5 | from crowdin_api.api_resources.abstract.resources import BaseResource 6 | from crowdin_api.requester import APIRequester 7 | 8 | 9 | class TestBaseResource: 10 | def test_get_project_id(self, base_absolut_url): 11 | project_id = 1 12 | resource = BaseResource( 13 | requester=APIRequester(base_url=base_absolut_url), 14 | project_id=project_id, 15 | ) 16 | assert resource.get_project_id() == project_id 17 | 18 | @pytest.mark.parametrize( 19 | "in_params,out_params", 20 | ( 21 | ({}, {"limit": 25, "offset": 0}), 22 | ({"page": 1}, {"limit": 25, "offset": 0}), 23 | ({"page": 2}, {"limit": 25, "offset": 25}), 24 | ({"limit": 100, "offset": 0}, {"limit": 100, "offset": 0}), 25 | ), 26 | ) 27 | def test_get_page_params_mixin(self, in_params, out_params, base_absolut_url): 28 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 29 | 30 | assert resource.get_page_params(**in_params) == out_params 31 | 32 | @pytest.mark.parametrize( 33 | "kwargs", 34 | ( 35 | {"page": 1, "limit": 25, "offset": 0}, 36 | {"page": -1}, 37 | {"limit": -1, "offset": 0}, 38 | {"limit": 25, "offset": -1}, 39 | ), 40 | ) 41 | def test_get_page_params_invalid_params(self, kwargs, base_absolut_url): 42 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 43 | with pytest.raises(ValueError): 44 | resource.get_page_params(**kwargs) 45 | 46 | @pytest.mark.parametrize( 47 | "in_param,out_param", 48 | ( 49 | ({"max_limit": None}, None), 50 | ({"max_limit": 0}, 0), 51 | ({"max_limit": 1}, 1), 52 | ({"max_limit": 100}, 100), 53 | ), 54 | ) 55 | def test_with_fetch_all(self, in_param, out_param, base_absolut_url): 56 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 57 | 58 | resource.with_fetch_all(**in_param) 59 | 60 | assert resource._max_limit == out_param 61 | assert resource._flag_fetch_all is True 62 | 63 | @pytest.mark.parametrize( 64 | "incoming_data, request_data", 65 | ( 66 | ( 67 | {"method": "get", "path": ""}, 68 | {"method": "get", "path": "", "params": None}, 69 | ), 70 | ( 71 | {"method": "get", "path": "test", "params": "params"}, 72 | {"method": "get", "path": "test", "params": "params"}, 73 | ), 74 | ), 75 | ) 76 | @mock.patch("crowdin_api.requester.APIRequester.request") 77 | def test__get_list(self, m_request, incoming_data, request_data, base_absolut_url): 78 | m_request.return_value = "response" 79 | 80 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 81 | 82 | assert resource._get_entire_data(**incoming_data) == "response" 83 | m_request.assert_called_once_with(**request_data) 84 | 85 | @pytest.mark.parametrize( 86 | "max_limit, incoming_data, request_data", 87 | ( 88 | ( 89 | None, 90 | {"method": "get", "path": ""}, 91 | {"method": "get", "path": "", "params": None, "max_amount": None}, 92 | ), 93 | ( 94 | 0, 95 | {"method": "get", "path": "test", "params": "params"}, 96 | {"method": "get", "path": "test", "params": "params", "max_amount": 0}, 97 | ), 98 | ( 99 | 1, 100 | {"method": "get", "path": "test", "params": "params"}, 101 | {"method": "get", "path": "test", "params": "params", "max_amount": 1}, 102 | ), 103 | ), 104 | ) 105 | def test__get_list_with__flag_fetch_all( 106 | self, 107 | incoming_data, 108 | max_limit, 109 | request_data, 110 | base_absolut_url 111 | ): 112 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 113 | resource.with_fetch_all(max_limit=max_limit) 114 | 115 | resource._fetch_all = Mock(return_value="response") 116 | 117 | assert resource._flag_fetch_all is True 118 | assert resource._get_entire_data(**incoming_data) == "response" 119 | assert resource._flag_fetch_all is False 120 | assert resource._max_limit is None 121 | resource._fetch_all.assert_called_once_with(**request_data) 122 | 123 | @pytest.mark.parametrize( 124 | "incoming_data, expected_result", 125 | ( 126 | ( 127 | {"method": "get", "path": ""}, 128 | {"data": [None] * 1} 129 | ), 130 | ( 131 | {"method": "get", "path": ""}, 132 | {"data": [None] * 499} 133 | ), 134 | ( 135 | {"method": "get", "path": "", "params": None, "max_amount": 0}, 136 | {"data": []} 137 | ), 138 | ( 139 | {"method": "get", "path": "", "params": None, "max_amount": 1}, 140 | {"data": [None] * 1} 141 | ), 142 | ( 143 | {"method": "get", "path": "", "params": None, "max_amount": 499}, 144 | {"data": [None] * 499} 145 | ), 146 | ), 147 | ) 148 | @mock.patch("crowdin_api.requester.APIRequester.request") 149 | def test__fetch_all(self, m_request, incoming_data, expected_result, base_absolut_url): 150 | m_request.return_value = expected_result 151 | resource = BaseResource(requester=APIRequester(base_url=base_absolut_url)) 152 | 153 | testing_result = resource._fetch_all(**incoming_data) 154 | assert testing_result == expected_result 155 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/ai/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/ai/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class AIPromptAction(Enum): 5 | ASSIST = "assist" 6 | PRE_TRANSLATE = "pre_translate" 7 | 8 | 9 | class AIPromptOperation(Enum): 10 | REPLACE = "replace" 11 | TEST = "test" 12 | 13 | 14 | class EditAIPromptPath(Enum): 15 | NAME = "/name" 16 | ACTION = "/action" 17 | AI_PROVIDER_ID = "/aiProviderId" 18 | AI_MODEL_ID = "/aiModelId" 19 | IS_ENABLED = "/isEnabled" 20 | ENABLED_PROJECT_IDS = "/enabledProjectIds" 21 | CONFIG = "/config" 22 | 23 | 24 | class AIProviderType(Enum): 25 | OPEN_AI = "open_ai" 26 | AZUER_OPEN_AI = "azure_open_ai" 27 | GOOGLE_GEMINI = "google_gemini" 28 | MISTRAL_AI = "mistral_ai" 29 | ANTHROPIC = "anthropic" 30 | CUSTOM_AI = "custom_ai" 31 | 32 | 33 | class EditAIProviderPath(Enum): 34 | NAME = "/name" 35 | TYPE = "/type" 36 | CREDENTIALS = "/credentials" 37 | CONFIG = "/config" 38 | IS_ENABLED = "/isEnabled" 39 | USE_SYSTEM_CREDENTIALS = "/useSystemCredentials" 40 | 41 | 42 | class DatasetPurpose(Enum): 43 | TRAINING = "training" 44 | VALIDATION = "validation" 45 | 46 | 47 | class EditAiCustomPlaceholderPatchPath(Enum): 48 | DESCRIPTION = "/description" 49 | PLACEHOLDER = "/placeholder" 50 | VALUE = "/value" 51 | 52 | 53 | class AiPromptFineTuningJobStatus(Enum): 54 | CREATED = "created" 55 | IN_PROGRESS = "in_progress" 56 | CANCELED = "canceled" 57 | FAILED = "failed" 58 | FINISHED = "finished" 59 | 60 | 61 | class AiToolType(Enum): 62 | FUNCTION = "function" 63 | 64 | 65 | class AiReportType(Enum): 66 | TOKENS_USAGE_RAW_DATA = "tokens-usage-raw-data" 67 | 68 | 69 | class EditAiSettingsPatchPath(Enum): 70 | ASSIST_ACTION_AI_PROMPT_ID = "/assistActionAiPromptId" 71 | EDITOR_SUGGESTION_AI_PROMPT_ID = "/editorSuggestionAiPromptId" 72 | SHORTCUTS = "/shortcuts" 73 | 74 | 75 | class ListAiPromptFineTuningJobsOrderBy(Enum): 76 | CREATED_AT = "createdAt" 77 | UPDATED_AT = "updatedAt" 78 | STARTED_AT = "startedAt" 79 | FINISHED_AT = "finishedAt" 80 | 81 | 82 | class AiReportFormat(Enum): 83 | CSV = "csv" 84 | JSON = "json" 85 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/application/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/application/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class UserPermissions(Enum): 5 | OWNER = "owner" 6 | MANAGERS = "managers" 7 | ALL = "all" 8 | GUESTS = "guests" 9 | RESTRICTED = "restricted" 10 | 11 | 12 | class ProjectPermissions(Enum): 13 | OWN = "own" 14 | RESTRICTED = "restricted" 15 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/application/types.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from crowdin_api.typing import TypedDict 3 | from crowdin_api.api_resources.application.enums import ( 4 | UserPermissions, 5 | ProjectPermissions, 6 | ) 7 | 8 | 9 | class ApplicationUser(TypedDict): 10 | value: UserPermissions 11 | ids: Iterable[int] 12 | 13 | 14 | class ApplicationProject(TypedDict): 15 | value: ProjectPermissions 16 | ids: Iterable[int] 17 | 18 | 19 | class ApplicationPermissions(TypedDict): 20 | user: ApplicationUser 21 | project: ApplicationProject 22 | 23 | 24 | class ApplicationInstallationPatchRequest(TypedDict): 25 | op: str 26 | path: str 27 | value: str 28 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/branches/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/branches/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EditBranchPatchPath(Enum): 5 | NAME = "/name" 6 | TITLE = "/title" 7 | PRIORITY = "/priority" 8 | 9 | 10 | class ListBranchesOrderBy(Enum): 11 | ID = "id" 12 | NAME = "name" 13 | TITLE = "title" 14 | CREATED_AT = "createdAt" 15 | UPDATED_AT = "updatedAt" 16 | EXPORT_PATTERN = "exportPattern" 17 | PRIORITY = "priority" 18 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/branches/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, Optional, Any 2 | 3 | from crowdin_api.api_resources.branches.enums import EditBranchPatchPath 4 | from crowdin_api.api_resources.enums import PatchOperation 5 | 6 | 7 | class CloneBranchRequest(TypedDict): 8 | name: str 9 | title: Optional[str] 10 | 11 | 12 | class AddBranchRequest(TypedDict): 13 | name: str 14 | title: Optional[str] 15 | 16 | 17 | class EditBranchPatch(TypedDict): 18 | op: PatchOperation 19 | path: EditBranchPatchPath 20 | value: Any 21 | 22 | 23 | class MergeBranchRequest(TypedDict): 24 | deleteAfterMerge: Optional[bool] 25 | sourceBranchId: int 26 | dryRun: Optional[bool] 27 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/bundles/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/bundles/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class BundlePatchPath(Enum): 5 | NAME = "/name" 6 | FORMAT = "/format" 7 | SOURCE_PATTERNS = "/sourcePatterns" 8 | IGNORE_PATTERNS = "/ignorePatterns" 9 | EXPORT_PATTERNS = "/exportPattern" 10 | DESCRIPTION = "/description" 11 | LABEL_IDS = "/labelIds" 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/bundles/types.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from crowdin_api.api_resources.bundles.enums import BundlePatchPath 4 | from crowdin_api.api_resources.enums import PatchOperation 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class BundlePatchRequest(TypedDict): 9 | value: Union[str, int] 10 | op: PatchOperation 11 | path: BundlePatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/clients/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/clients/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | 5 | 6 | class ClientsResource(BaseResource): 7 | """ 8 | Resource for Clients. 9 | 10 | Link to documentation for enterprise: 11 | https://developer.crowdin.com/enterprise/api/v2/#tag/Clients 12 | """ 13 | 14 | def list_clients( 15 | self, 16 | limit: Optional[int] = None, 17 | offset: Optional[int] = None 18 | ): 19 | """ 20 | List Clients 21 | 22 | Link to documentation for enterprise: 23 | https://support.crowdin.com/developer/enterprise/api/v2/#tag/Clients/operation/api.clients.getMany 24 | """ 25 | 26 | return self.requester.request( 27 | method="get", 28 | path="/clients", 29 | params=self.get_page_params(offset=offset, limit=limit) 30 | ) 31 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/dictionaries/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/dictionaries/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.dictionaries.types import DictionaryPatchPath 5 | 6 | 7 | class DictionariesResource(BaseResource): 8 | """ 9 | Resource for Dictionaries. 10 | 11 | Dictionaries allow you to create a storage of words that should be skipped by the spell checker. 12 | 13 | Use API to get the list of organization dictionaries and to edit a specific dictionary. 14 | 15 | Link to documentation: 16 | https://developer.crowdin.com/api/v2/#tag/Dictionaries 17 | """ 18 | 19 | def list_dictionaries( 20 | self, 21 | projectId: Optional[int] = None, 22 | languageIds: Optional[Iterable[str]] = None, 23 | page: Optional[int] = None, 24 | offset: Optional[int] = None, 25 | limit: Optional[int] = None, 26 | ): 27 | """ 28 | List Dictionaries. 29 | 30 | Link to documentation: 31 | https://developer.crowdin.com/api/v2/#operation/api.projects.dictionaries.getMany 32 | """ 33 | 34 | params = self.get_page_params(page=page, offset=offset, limit=limit) 35 | params["languageIds"] = None if languageIds is None else ",".join(languageIds) 36 | projectId = projectId or self.get_project_id() 37 | 38 | return self._get_entire_data( 39 | method="get", 40 | path=f"projects/{projectId}/dictionaries", 41 | params=params, 42 | ) 43 | 44 | def edit_dictionary( 45 | self, 46 | languageId: str, 47 | data: Iterable[DictionaryPatchPath], 48 | projectId: Optional[int] = None, 49 | ): 50 | """ 51 | Edit Dictionary. 52 | 53 | Link to documentation: 54 | https://developer.crowdin.com/api/v2/#operation/api.projects.dictionaries.patch 55 | """ 56 | 57 | projectId = projectId or self.get_project_id() 58 | 59 | return self.requester.request( 60 | method="patch", 61 | path=f"projects/{projectId}/dictionaries/{languageId}", 62 | request_data=data, 63 | ) 64 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/dictionaries/tests/test_dictionaries_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from crowdin_api.api_resources.dictionaries.resource import DictionariesResource 5 | from crowdin_api.api_resources.enums import PatchOperation 6 | from crowdin_api.requester import APIRequester 7 | 8 | 9 | class TestDictionariesResource: 10 | resource_class = DictionariesResource 11 | 12 | def get_resource(self, base_absolut_url): 13 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 14 | 15 | def test_resource_with_project_id(self, base_absolut_url): 16 | project_id = 1 17 | resource = self.resource_class( 18 | requester=APIRequester(base_url=base_absolut_url), project_id=project_id 19 | ) 20 | assert resource.get_project_id() == project_id 21 | 22 | @pytest.mark.parametrize( 23 | "incoming_data, request_params", 24 | ( 25 | ( 26 | {}, 27 | { 28 | "languageIds": None, 29 | "offset": 0, 30 | "limit": 25, 31 | }, 32 | ), 33 | ( 34 | {"languageIds": ["ua", "en", "pl"]}, 35 | { 36 | "languageIds": "ua,en,pl", 37 | "offset": 0, 38 | "limit": 25, 39 | }, 40 | ), 41 | ), 42 | ) 43 | @mock.patch("crowdin_api.requester.APIRequester.request") 44 | def test_list_dictionaries(self, m_request, incoming_data, request_params, base_absolut_url): 45 | m_request.return_value = "response" 46 | 47 | resource = self.get_resource(base_absolut_url) 48 | assert resource.list_dictionaries(projectId=1, **incoming_data) == "response" 49 | m_request.assert_called_once_with( 50 | method="get", 51 | params=request_params, 52 | path="projects/1/dictionaries", 53 | ) 54 | 55 | @mock.patch("crowdin_api.requester.APIRequester.request") 56 | def test_edit_custom_language(self, m_request, base_absolut_url): 57 | m_request.return_value = "response" 58 | 59 | data = [ 60 | { 61 | "op": PatchOperation.REPLACE, 62 | "path": "/words/0", 63 | } 64 | ] 65 | 66 | resource = self.get_resource(base_absolut_url) 67 | assert resource.edit_dictionary(projectId=1, languageId="ua", data=data) == "response" 68 | m_request.assert_called_once_with( 69 | method="patch", request_data=data, path="projects/1/dictionaries/ua" 70 | ) 71 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/dictionaries/types.py: -------------------------------------------------------------------------------- 1 | from crowdin_api.api_resources.enums import PatchOperation 2 | from crowdin_api.typing import TypedDict 3 | 4 | 5 | class DictionaryPatchPath(TypedDict): 6 | op: PatchOperation 7 | path: str 8 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/distributions/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/distributions/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DistributionPatchPath(Enum): 5 | EXPORT_MODE = "/exportMode" 6 | NAME = "/name" 7 | FILE_IDS = "/fileIds" 8 | BUNDLE_IDS = "/bundleIds" 9 | 10 | 11 | class ExportMode(Enum): 12 | DEFAULT = "default" 13 | BUNDLE = "bundle" 14 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/distributions/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.distributions.enums import ExportMode 5 | from crowdin_api.api_resources.distributions.types import DistributionPatchRequest 6 | 7 | 8 | class DistributionsResource(BaseResource): 9 | """ 10 | Resource for Distributions. 11 | 12 | Link to documentation: 13 | https://developer.crowdin.com/api/v2/#tag/Distributions 14 | """ 15 | 16 | def get_distributions_path(self, projectId: int, hash: Optional[str] = None): 17 | if hash: 18 | return f"projects/{projectId}/distributions/{hash}" 19 | 20 | return f"projects/{projectId}/distributions" 21 | 22 | def list_distributions( 23 | self, 24 | projectId: Optional[int] = None, 25 | offset: Optional[int] = None, 26 | limit: Optional[int] = None, 27 | ): 28 | """ 29 | List Distributions. 30 | 31 | Link to documentation: 32 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.getMany 33 | """ 34 | 35 | projectId = projectId or self.get_project_id() 36 | 37 | return self._get_entire_data( 38 | method="get", 39 | path=self.get_distributions_path(projectId=projectId), 40 | params=self.get_page_params(offset=offset, limit=limit), 41 | ) 42 | 43 | def add_distribution( 44 | self, 45 | name: str, 46 | projectId: Optional[int] = None, 47 | fileIds: Optional[Iterable[int]] = None, 48 | bundleIds: Optional[Iterable[int]] = None, 49 | exportMode: Optional[ExportMode] = ExportMode.DEFAULT, 50 | ): 51 | """ 52 | Add Distribution. 53 | 54 | Link to documentation: 55 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.post 56 | """ 57 | 58 | projectId = projectId or self.get_project_id() 59 | 60 | return self.requester.request( 61 | method="post", 62 | path=self.get_distributions_path(projectId=projectId), 63 | request_data={ 64 | "exportMode": exportMode, 65 | "name": name, 66 | "fileIds": fileIds, 67 | "bundleIds": bundleIds 68 | }, 69 | ) 70 | 71 | def get_distribution(self, hash: str, projectId: Optional[int] = None): 72 | """ 73 | Get Distribution. 74 | 75 | Link to documentation: 76 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.get 77 | """ 78 | 79 | projectId = projectId or self.get_project_id() 80 | 81 | return self.requester.request( 82 | method="get", 83 | path=self.get_distributions_path(projectId=projectId, hash=hash), 84 | ) 85 | 86 | def delete_distribution(self, hash: str, projectId: Optional[int] = None): 87 | """ 88 | Delete Distribution. 89 | 90 | Link to documentation: 91 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.delete 92 | """ 93 | 94 | projectId = projectId or self.get_project_id() 95 | 96 | return self.requester.request( 97 | method="delete", 98 | path=self.get_distributions_path(projectId=projectId, hash=hash), 99 | ) 100 | 101 | def edit_distribution( 102 | self, 103 | hash: str, 104 | data: Iterable[DistributionPatchRequest], 105 | projectId: Optional[int] = None, 106 | ): 107 | """ 108 | Edit Distribution. 109 | 110 | Link to documentation: 111 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.patch 112 | """ 113 | 114 | projectId = projectId or self.get_project_id() 115 | 116 | return self.requester.request( 117 | method="patch", 118 | path=self.get_distributions_path(projectId=projectId, hash=hash), 119 | request_data=data, 120 | ) 121 | 122 | def get_distribution_release(self, hash: str, projectId: Optional[int] = None): 123 | """ 124 | Get Distribution Release. 125 | 126 | Link to documentation: 127 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.release.get 128 | """ 129 | 130 | projectId = projectId or self.get_project_id() 131 | 132 | return self.requester.request( 133 | method="get", 134 | path=f"{self.get_distributions_path(projectId=projectId, hash=hash)}/release", 135 | ) 136 | 137 | def release_distribution(self, hash: str, projectId: Optional[int] = None): 138 | """ 139 | Release Distribution. 140 | 141 | Link to documentation: 142 | https://developer.crowdin.com/api/v2/#operation/api.projects.distributions.release.post 143 | """ 144 | 145 | projectId = projectId or self.get_project_id() 146 | 147 | return self.requester.request( 148 | method="post", 149 | path=f"{self.get_distributions_path(projectId=projectId, hash=hash)}/release", 150 | ) 151 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/distributions/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.distributions.enums import DistributionPatchPath 4 | from crowdin_api.api_resources.enums import PatchOperation 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class DistributionPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: DistributionPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PatchOperation(Enum): 5 | ADD = "add" 6 | TEST = "test" 7 | REPLACE = "replace" 8 | REMOVE = "remove" 9 | 10 | 11 | class ExportFormat(Enum): 12 | TBX = "tbx" 13 | CSV = "csv" 14 | XLSX = "xlsx" 15 | 16 | 17 | class DenormalizePlaceholders(Enum): 18 | ENABLE = 1 19 | DISABLE = 0 20 | 21 | 22 | class PluralCategoryName(Enum): 23 | ZERO = "zero" 24 | ONE = "one" 25 | TWO = "two" 26 | FEW = "few" 27 | MANY = "many" 28 | OTHER = "other" 29 | 30 | 31 | class ExportProjectTranslationFormat(Enum): 32 | XLIFF = "xliff" 33 | ANDROID = "android" 34 | MACOSX = "macosx" 35 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/fields/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/fields/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FieldEntity(Enum): 5 | PROJECT = "project" 6 | USER = "user" 7 | TASK = "task" 8 | FILE = "file" 9 | TRANSLATION = "translation" 10 | STRING = "string" 11 | 12 | 13 | class FieldType(Enum): 14 | CHECKBOX = "checkbox" 15 | RADIOBUTTONS = "radiobuttons" 16 | DATE = "date" 17 | DATETIME = "datetime" 18 | NUMBER = "number" 19 | LABELS = "labels" 20 | SELECT = "select" 21 | MULTISELECT = "multiselect" 22 | TEXT = "text" 23 | TEXTAREA = "textarea" 24 | URL = "url" 25 | 26 | 27 | class FieldPlace(Enum): 28 | PROJECT_CREATE_MODAL = "projectCreateModal" 29 | PROJECT_HEADER = "projectHeader" 30 | PROJECT_DETAILS = "projectDetails" 31 | PROJECT_CROWDSOURCE_DETAILS = "projectCrowdsourceDetails" 32 | PROJECT_SETTINGS = "projectSettings" 33 | PROJECT_TASK_EDIT_CREATE = "projectTaskEditCreate" 34 | PROJECT_TASK_DETAILS = "projectTaskDetails" 35 | FILE_DETAILS = "fileDetails" 36 | FILE_SETTINGS = "fileSettings" 37 | USER_EDIT_MODAL = "userEditModal" 38 | USER_DETAILS = "userDetails" 39 | USER_POPOVER = "userPopover" 40 | STRING_EDIT_MODAL = "stringEditModal" 41 | STRING_DETAILS = "stringDetails" 42 | TRANSLATION_UNDER_CONTENT = "translationUnderContent" 43 | 44 | 45 | class FieldOperations(Enum): 46 | REPLACE = "replace" 47 | 48 | 49 | class FieldsPatchPath(Enum): 50 | NAME = "/name" 51 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/fields/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Iterable, Union 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.fields.enums import FieldEntity, FieldType 5 | from crowdin_api.api_resources.fields.types import ( 6 | ListFieldConfig, 7 | NumberFieldConfig, 8 | OtherFieldConfig, 9 | FieldPatchRequest, 10 | ) 11 | 12 | 13 | class FieldsResource(BaseResource): 14 | """ 15 | Resource for Fields. 16 | 17 | Link to documentation: 18 | https://developer.crowdin.com/enterprise/api/v2/#tag/Fields 19 | """ 20 | 21 | def get_fields_path(self, fieldId: Optional[int] = None): 22 | if fieldId is None: 23 | return "fields" 24 | return f"fields/{fieldId}" 25 | 26 | def list_fields( 27 | self, 28 | search: Optional[str] = None, 29 | entity: Optional[FieldEntity] = None, 30 | type: Optional[FieldType] = None, 31 | limit: Optional[int] = None, 32 | offset: Optional[int] = None, 33 | ): 34 | """ 35 | List Fields 36 | 37 | Link to documentation: 38 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.getMany 39 | """ 40 | params = {"search": search, "entity": entity, "type": type} 41 | params.update(self.get_page_params(limit=limit, offset=offset)) 42 | 43 | return self._get_entire_data( 44 | method="get", path=self.get_fields_path(), params=params 45 | ) 46 | 47 | def add_field( 48 | self, 49 | name: str, 50 | slug: str, 51 | type: FieldType, 52 | entities: Iterable[FieldEntity], 53 | description: Optional[str] = None, 54 | config: Optional[ 55 | Union[ListFieldConfig, NumberFieldConfig, OtherFieldConfig] 56 | ] = None, 57 | ): 58 | """ 59 | Add Field 60 | 61 | Link to documentation: 62 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.post 63 | """ 64 | data = { 65 | "name": name, 66 | "slug": slug, 67 | "type": type, 68 | "entities": entities, 69 | "description": description, 70 | "config": config, 71 | } 72 | 73 | return self.requester.request( 74 | method="post", path=self.get_fields_path(), request_data=data 75 | ) 76 | 77 | def get_field(self, fieldId: int): 78 | """ 79 | Get Field 80 | 81 | Link to documentaion: 82 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.get 83 | """ 84 | 85 | return self.requester.request( 86 | method="get", path=self.get_fields_path(fieldId=fieldId) 87 | ) 88 | 89 | def delete_field(self, fieldId: int): 90 | """ 91 | Delete Field 92 | 93 | Link to documetation: 94 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.delete 95 | """ 96 | 97 | return self.requester.request( 98 | method="delete", path=self.get_fields_path(fieldId=fieldId) 99 | ) 100 | 101 | def edit_field(self, fieldId: int, data: Iterable[FieldPatchRequest]): 102 | """ 103 | Edit Field 104 | 105 | Link to documentation: 106 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.fields.patch 107 | """ 108 | 109 | return self.requester.request( 110 | method="patch", 111 | path=self.get_fields_path(fieldId=fieldId), 112 | request_data=data, 113 | ) 114 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/fields/types.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Any 2 | from crowdin_api.typing import TypedDict 3 | from crowdin_api.api_resources.fields.enums import ( 4 | FieldPlace, 5 | FieldOperations, 6 | FieldsPatchPath, 7 | ) 8 | 9 | 10 | class FieldOptions(TypedDict): 11 | label: str 12 | value: str 13 | 14 | 15 | class FieldLocation(TypedDict): 16 | place: FieldPlace 17 | 18 | 19 | class ListFieldConfig(TypedDict): 20 | options: Iterable[FieldOptions] 21 | locations: Iterable[FieldPlace] 22 | 23 | 24 | class NumberFieldConfig(TypedDict): 25 | min: int 26 | max: int 27 | units: str 28 | locations: Iterable[FieldLocation] 29 | 30 | 31 | class OtherFieldConfig(TypedDict): 32 | locations: Iterable[FieldLocation] 33 | 34 | 35 | class FieldPatchRequest(TypedDict): 36 | op: FieldOperations 37 | path: FieldsPatchPath 38 | value: Any 39 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/glossaries/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/glossaries/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GlossaryPatchPath(Enum): 5 | NAME = "/name" 6 | 7 | 8 | class GlossaryFormat(Enum): 9 | TBX = "tbx" 10 | TBX_V3 = "tbx_v3" 11 | CSV = "csv" 12 | XLSX = "xlsx" 13 | 14 | 15 | class GlossaryExportFields(Enum): 16 | TERM = "term" 17 | DESCRIPTION = "description" 18 | PART_OF_SPEECH = "partOfSpeech" 19 | TYPE = "type" 20 | STATUS = "status" 21 | GENDER = "gender" 22 | NOTE = "note" 23 | URL = "url" 24 | 25 | 26 | class TermPatchPath(Enum): 27 | TEXT = "/text" 28 | DESCRIPTION = "/description" 29 | PART_OF_SPEECH = "/partOfSpeech" 30 | STATUS = "/status" 31 | TYPE = "/type" 32 | GENDER = "/gender" 33 | URL = "/url" 34 | NOTE = "/note" 35 | 36 | 37 | class TermPartOfSpeech(Enum): 38 | ADJECTIVE = "adjective" 39 | ADPOSITION = "adposition" 40 | ADVERB = "adverb" 41 | AUXILIARY = "auxiliary" 42 | COORDINATING_CONJUNCTION = "coordinating conjunction" 43 | DETERMINER = "determiner" 44 | INTERJECTION = "interjection" 45 | NOUN = "noun" 46 | NUMERAL = "numeral" 47 | PARTICLE = "particle" 48 | PRONOUN = "pronoun" 49 | PROPER_NOUN = "proper noun" 50 | SUBORDINATING_CONJUNCTION = "subordinating conjunction" 51 | VERB = "verb" 52 | OTHER = "other" 53 | 54 | 55 | class TermStatus(Enum): 56 | PREFERRED = "preferred" 57 | ADMITTED = "admitted" 58 | NOT_RECOMMEND = "not recommended" 59 | OBSOLETE = "obsolete" 60 | 61 | 62 | class TermType(Enum): 63 | FULL_FORM = "full form" 64 | ACRONYM = "acronym" 65 | ABBREVIATION = "abbreviation" 66 | SHORT_FORM = "short form" 67 | PHRASE = "phrase" 68 | VARIANT = "variant" 69 | 70 | 71 | class TermGender(Enum): 72 | MASCULINE = "masculine" 73 | FEMININE = "feminine" 74 | NEUTER = "neuter" 75 | OTHER = "other" 76 | 77 | 78 | class ListConceptsOrderBy(Enum): 79 | ID = "id" 80 | SUBJECT = "subject" 81 | DEFINITION = "definition" 82 | NOTE = "note" 83 | CREATED_AT = "createdAt" 84 | UPDATED_AT = "updatedAt" 85 | 86 | 87 | class ListGlossariesCrowdinOrderBy(Enum): 88 | ID = "id" 89 | NAME = "name" 90 | USER_ID = "userId" 91 | CREATED_AT = "createdAt" 92 | 93 | 94 | class ListGlossariesEnterpriseOrderBy(Enum): 95 | ID = "id" 96 | NAME = "name" 97 | GROUP_ID = "groupId" 98 | USER_ID = "userId" 99 | CREATED_AT = "createdAt" 100 | 101 | 102 | class ListTermsOrderBy(Enum): 103 | ID = "id" 104 | TEXT = "text" 105 | DESCRIPTION = "description" 106 | PART_OF_SPEECH = "partOfSpeech" 107 | STATUS = "status" 108 | TYPE = "type" 109 | GENDER = "gender" 110 | NOTE = "note" 111 | LEMMA = "lemma" 112 | CREATED_AT = "createdAt" 113 | UPDATED_AT = "updatedAt" 114 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/glossaries/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Iterable 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.glossaries.enums import ( 5 | GlossaryPatchPath, 6 | TermPatchPath, 7 | GlossaryFormat, 8 | GlossaryExportFields, 9 | ) 10 | from crowdin_api.typing import TypedDict 11 | 12 | 13 | class GlossaryPatchRequest(TypedDict): 14 | value: Any 15 | op: PatchOperation 16 | path: GlossaryPatchPath 17 | 18 | 19 | class GlossarySchemaRequest(TypedDict): 20 | format: Optional[GlossaryFormat] 21 | exportFields: Optional[Iterable[GlossaryExportFields]] 22 | 23 | 24 | class TermPatchRequest(TypedDict): 25 | value: Any 26 | op: PatchOperation 27 | path: TermPatchPath 28 | 29 | 30 | class LanguagesDetails(TypedDict): 31 | languageId: str 32 | definition: str 33 | note: Optional[str] 34 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/groups/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/groups/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GroupPatchPath(Enum): 5 | NAME = "/name" 6 | DESCRIPTION = "/description" 7 | PARENT_ID = "/parentId" 8 | 9 | 10 | class ListGroupsOrderBy(Enum): 11 | ID = "id" 12 | NAME = "name" 13 | DESCRIPTION = "description" 14 | CREATED_AT = "createdAt" 15 | UPDATED_AT = "updatedAt" 16 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/groups/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Iterable 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.groups.types import GroupPatchRequest 5 | from crowdin_api.sorting import Sorting 6 | 7 | 8 | class GroupsResource(BaseResource): 9 | """ 10 | Resource for Groups. 11 | 12 | Groups allow you to organize your projects based on specific characteristics. 13 | Using projects, you can keep your source files sorted. 14 | 15 | Use API to manage projects and groups, change their settings, or remove them from 16 | organization if required. 17 | 18 | Link to documentation: 19 | https://developer.crowdin.com/enterprise/api/v2/#tag/Projects-and-Groups 20 | """ 21 | 22 | # Glossaries 23 | def get_groups_path(self, groupId: Optional[int] = None): 24 | if groupId is not None: 25 | return f"groups/{groupId}" 26 | 27 | return "groups" 28 | 29 | def get_group(self, groupId: int): 30 | """ 31 | Get Group. 32 | 33 | Link to documentation: 34 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.get 35 | """ 36 | 37 | return self.requester.request( 38 | method="get", 39 | path=self.get_groups_path(groupId=groupId), 40 | ) 41 | 42 | def add_group( 43 | self, 44 | name: str, 45 | parentId: Optional[int] = None, 46 | description: Optional[str] = None 47 | ): 48 | """ 49 | Add Group. 50 | 51 | Link to documentation: 52 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.post 53 | """ 54 | 55 | return self.requester.request( 56 | method="post", 57 | path=self.get_groups_path(), 58 | request_data={ 59 | "name": name, 60 | "parentId": parentId, 61 | "description": description 62 | } 63 | ) 64 | 65 | def list_groups( 66 | self, 67 | orderBy: Optional[Sorting] = None, 68 | parentId: Optional[int] = None, 69 | limit: Optional[int] = None, 70 | offset: Optional[int] = None 71 | ): 72 | """ 73 | List Groups. 74 | 75 | Link to documentation: 76 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.getMany 77 | """ 78 | 79 | params = {"orderBy": orderBy, "parentId": parentId} 80 | params.update(self.get_page_params(offset=offset, limit=limit)) 81 | 82 | return self._get_entire_data( 83 | method="get", 84 | path=self.get_groups_path(), 85 | params=params, 86 | ) 87 | 88 | def edit_group(self, groupId: int, data: Iterable[GroupPatchRequest]): 89 | """ 90 | Edit Group. 91 | 92 | Link to documentation: 93 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.patch 94 | """ 95 | 96 | return self.requester.request( 97 | method="patch", 98 | path=self.get_groups_path(groupId=groupId), 99 | request_data=data, 100 | ) 101 | 102 | def delete_group(self, groupId: int): 103 | """ 104 | Delete Group. 105 | 106 | Link to documentation: 107 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.groups.delete 108 | """ 109 | 110 | return self.requester.request( 111 | method="delete", 112 | path=self.get_groups_path(groupId=groupId), 113 | ) 114 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/groups/tests/test_groups_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from crowdin_api.api_resources import GroupsResource 6 | from crowdin_api.api_resources.enums import PatchOperation 7 | from crowdin_api.api_resources.groups.enums import GroupPatchPath, ListGroupsOrderBy 8 | from crowdin_api.requester import APIRequester 9 | from crowdin_api.sorting import Sorting, SortingOrder, SortingRule 10 | 11 | 12 | class TestGroupsResource: 13 | resource_class = GroupsResource 14 | 15 | def get_resource(self, base_absolut_url): 16 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 17 | 18 | @pytest.mark.parametrize( 19 | "in_params, path", 20 | ( 21 | ({}, "groups"), 22 | ({"groupId": 1}, "groups/1"), 23 | ), 24 | ) 25 | def test_get_groups_path(self, in_params, path, base_absolut_url): 26 | resource = self.get_resource(base_absolut_url) 27 | assert resource.get_groups_path(**in_params) == path 28 | 29 | @mock.patch("crowdin_api.requester.APIRequester.request") 30 | def test_get_group(self, m_request, base_absolut_url): 31 | m_request.return_value = "response" 32 | 33 | resource = self.get_resource(base_absolut_url) 34 | assert resource.get_group(groupId=1) == "response" 35 | m_request.assert_called_once_with( 36 | method="get", path=resource.get_groups_path(groupId=1) 37 | ) 38 | 39 | @mock.patch("crowdin_api.requester.APIRequester.request") 40 | def test_delete_group(self, m_request, base_absolut_url): 41 | m_request.return_value = "response" 42 | 43 | resource = self.get_resource(base_absolut_url) 44 | assert resource.delete_group(groupId=1) == "response" 45 | m_request.assert_called_once_with( 46 | method="delete", path=resource.get_groups_path(groupId=1) 47 | ) 48 | 49 | @pytest.mark.parametrize( 50 | "incoming_data, request_params", 51 | ( 52 | ( 53 | {}, 54 | { 55 | "orderBy": None, 56 | "parentId": None, 57 | "limit": 25, 58 | "offset": 0, 59 | }, 60 | ), 61 | ( 62 | { 63 | "orderBy": Sorting( 64 | [SortingRule(ListGroupsOrderBy.NAME, SortingOrder.DESC)] 65 | ), 66 | "parentId": "test", 67 | "limit": 10, 68 | "offset": 2, 69 | }, 70 | { 71 | "orderBy": Sorting( 72 | [SortingRule(ListGroupsOrderBy.NAME, SortingOrder.DESC)] 73 | ), 74 | "parentId": "test", 75 | "limit": 10, 76 | "offset": 2, 77 | }, 78 | ), 79 | ), 80 | ) 81 | @mock.patch("crowdin_api.requester.APIRequester.request") 82 | def test_list_groups(self, m_request, incoming_data, request_params, base_absolut_url): 83 | m_request.return_value = "response" 84 | 85 | resource = self.get_resource(base_absolut_url) 86 | assert resource.list_groups(**incoming_data) == "response" 87 | m_request.assert_called_once_with( 88 | method="get", 89 | path=resource.get_groups_path(), 90 | params=request_params, 91 | ) 92 | 93 | @pytest.mark.parametrize( 94 | "incoming_data, request_params", 95 | ( 96 | ( 97 | {"name": "test_name"}, 98 | { 99 | "name": "test_name", 100 | "parentId": None, 101 | "description": None, 102 | }, 103 | ), 104 | ( 105 | { 106 | "name": "test_name", 107 | "parentId": 2, 108 | "description": "some text", 109 | }, 110 | { 111 | "name": "test_name", 112 | "parentId": 2, 113 | "description": "some text", 114 | }, 115 | ), 116 | ), 117 | ) 118 | @mock.patch("crowdin_api.requester.APIRequester.request") 119 | def test_add_group(self, m_request, incoming_data, request_params, base_absolut_url): 120 | m_request.return_value = "response" 121 | 122 | resource = self.get_resource(base_absolut_url) 123 | assert resource.add_group(**incoming_data) == "response" 124 | m_request.assert_called_once_with( 125 | method="post", 126 | path=resource.get_groups_path(), 127 | request_data=request_params, 128 | ) 129 | 130 | @mock.patch("crowdin_api.requester.APIRequester.request") 131 | def test_edit_group(self, m_request, base_absolut_url): 132 | m_request.return_value = "response" 133 | 134 | data = [ 135 | { 136 | "value": "test", 137 | "op": PatchOperation.REPLACE, 138 | "path": GroupPatchPath.NAME, 139 | } 140 | ] 141 | 142 | resource = self.get_resource(base_absolut_url) 143 | assert resource.edit_group(groupId=1, data=data) == "response" 144 | m_request.assert_called_once_with( 145 | method="patch", 146 | request_data=data, 147 | path=resource.get_groups_path(groupId=1), 148 | ) 149 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/groups/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.groups.enums import GroupPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class GroupPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: GroupPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/labels/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/labels/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LabelsPatchPath(Enum): 5 | TITLE = "/title" 6 | 7 | 8 | class ListLabelsOrderBy(Enum): 9 | ID = "id" 10 | TITLE = "title" 11 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/labels/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.labels.enums import LabelsPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class LabelsPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: LabelsPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/languages/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/languages/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LanguageTextDirection(Enum): 5 | LEFT_TO_RIGHT = "ltr" 6 | RIGHT_TO_LEFT = "rtl" 7 | 8 | 9 | class LanguagesPatchPath(Enum): 10 | NAME = "/name" 11 | TEXT_DIRECTION = "/textDirection" 12 | PLURAL_CATEGORY_NAMES = "/pluralCategoryNames" 13 | THREE_LETTERS_CODE = "/threeLettersCode" 14 | LOCALE_CODE = "/localeCode" 15 | DIALECT_OF = "/dialectOf" 16 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/languages/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.languages.enums import LanguageTextDirection 5 | from crowdin_api.api_resources.languages.types import LanguagesPatchRequest 6 | 7 | 8 | class LanguagesResource(BaseResource): 9 | """ 10 | Resource for Languages. 11 | 12 | Crowdin supports more than 300 world languages and custom languages created in the system. 13 | 14 | Use API to get the list of all supported languages and retrieve additional details 15 | (e.g. text direction, internal code) on specific language. 16 | 17 | Link to documentation: https://developer.crowdin.com/api/v2/#tag/Languages 18 | """ 19 | 20 | def get_languages_path(self, languageId: Optional[str] = None): 21 | if languageId: 22 | return f"languages/{languageId}" 23 | 24 | return "languages" 25 | 26 | def list_supported_languages( 27 | self, 28 | page: Optional[int] = None, 29 | offset: Optional[int] = None, 30 | limit: Optional[int] = None, 31 | ): 32 | """ 33 | List Supported Languages. 34 | 35 | Link to documentation: 36 | https://developer.crowdin.com/api/v2/#operation/api.languages.getMany 37 | """ 38 | 39 | return self._get_entire_data( 40 | method="get", 41 | path=self.get_languages_path(), 42 | params=self.get_page_params(page=page, offset=offset, limit=limit), 43 | ) 44 | 45 | def add_custom_language( 46 | self, 47 | name: str, 48 | code: str, 49 | localeCode: str, 50 | textDirection: LanguageTextDirection, 51 | pluralCategoryNames: Iterable[str], 52 | threeLettersCode: str, 53 | twoLettersCode: Optional[str] = None, 54 | dialectOf: Optional[str] = None, 55 | ): 56 | """ 57 | Add Custom Language. 58 | 59 | Link to documentation: 60 | https://developer.crowdin.com/api/v2/#operation/api.languages.post 61 | """ 62 | 63 | return self.requester.request( 64 | method="post", 65 | path=self.get_languages_path(), 66 | request_data={ 67 | "name": name, 68 | "code": code, 69 | "localeCode": localeCode, 70 | "textDirection": textDirection, 71 | "pluralCategoryNames": pluralCategoryNames, 72 | "threeLettersCode": threeLettersCode, 73 | "twoLettersCode": twoLettersCode, 74 | "dialectOf": dialectOf, 75 | }, 76 | ) 77 | 78 | def get_language(self, languageId: str): 79 | """ 80 | Get Language. 81 | 82 | Link to documentation: 83 | https://developer.crowdin.com/api/v2/#operation/api.languages.get 84 | """ 85 | 86 | return self.requester.request( 87 | method="get", path=self.get_languages_path(languageId=languageId) 88 | ) 89 | 90 | def delete_custom_language(self, languageId: str): 91 | """ 92 | Delete Custom Language. 93 | 94 | Link to documentation: 95 | https://developer.crowdin.com/api/v2/#operation/api.languages.delete 96 | """ 97 | 98 | return self.requester.request( 99 | method="delete", path=self.get_languages_path(languageId=languageId) 100 | ) 101 | 102 | def edit_custom_language(self, languageId: str, data: Iterable[LanguagesPatchRequest]): 103 | """ 104 | Edit Custom Language. 105 | 106 | Link to documentation: 107 | https://developer.crowdin.com/api/v2/#operation/api.languages.patch 108 | """ 109 | 110 | return self.requester.request( 111 | method="patch", 112 | path=self.get_languages_path(languageId=languageId), 113 | request_data=data, 114 | ) 115 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/languages/tests/test_languages_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from crowdin_api.api_resources import LanguagesResource 5 | from crowdin_api.api_resources.enums import PatchOperation 6 | from crowdin_api.api_resources.languages.enums import LanguagesPatchPath, LanguageTextDirection 7 | from crowdin_api.requester import APIRequester 8 | 9 | 10 | class TestLanguagesResource: 11 | resource_class = LanguagesResource 12 | 13 | def get_resource(self, base_absolut_url): 14 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 15 | 16 | @mock.patch("crowdin_api.requester.APIRequester.request") 17 | def test_list_supported_languages(self, m_request, base_absolut_url): 18 | m_request.return_value = "response" 19 | 20 | resource = self.get_resource(base_absolut_url) 21 | assert resource.list_supported_languages(page=10) == "response" 22 | m_request.assert_called_once_with( 23 | method="get", 24 | params=resource.get_page_params(page=10, offset=None, limit=None), 25 | path="languages", 26 | ) 27 | 28 | @pytest.mark.parametrize( 29 | "in_params, request_data", 30 | ( 31 | ( 32 | { 33 | "name": "Some", 34 | "code": "some", 35 | "localeCode": "some-sm", 36 | "textDirection": LanguageTextDirection.LEFT_TO_RIGHT, 37 | "pluralCategoryNames": ["one"], 38 | "threeLettersCode": "smm", 39 | }, 40 | { 41 | "name": "Some", 42 | "code": "some", 43 | "localeCode": "some-sm", 44 | "textDirection": LanguageTextDirection.LEFT_TO_RIGHT, 45 | "pluralCategoryNames": ["one"], 46 | "twoLettersCode": None, 47 | "dialectOf": None, 48 | "threeLettersCode": "smm", 49 | }, 50 | ), 51 | ( 52 | { 53 | "name": "Some", 54 | "code": "some", 55 | "localeCode": "some-sm", 56 | "textDirection": LanguageTextDirection.LEFT_TO_RIGHT, 57 | "pluralCategoryNames": ["one"], 58 | "twoLettersCode": "sm", 59 | "dialectOf": "uk", 60 | "threeLettersCode": "smm", 61 | }, 62 | { 63 | "name": "Some", 64 | "code": "some", 65 | "localeCode": "some-sm", 66 | "textDirection": LanguageTextDirection.LEFT_TO_RIGHT, 67 | "pluralCategoryNames": ["one"], 68 | "twoLettersCode": "sm", 69 | "dialectOf": "uk", 70 | "threeLettersCode": "smm", 71 | }, 72 | ), 73 | ), 74 | ) 75 | @mock.patch("crowdin_api.requester.APIRequester.request") 76 | def test_add_custom_language(self, m_request, in_params, request_data, base_absolut_url): 77 | m_request.return_value = "response" 78 | 79 | resource = self.get_resource(base_absolut_url) 80 | assert resource.add_custom_language(**in_params) == "response" 81 | m_request.assert_called_once_with( 82 | method="post", path="languages", request_data=request_data 83 | ) 84 | 85 | @mock.patch("crowdin_api.requester.APIRequester.request") 86 | def test_get_language(self, m_request, base_absolut_url): 87 | m_request.return_value = "response" 88 | 89 | resource = self.get_resource(base_absolut_url) 90 | assert resource.get_language(languageId=1) == "response" 91 | m_request.assert_called_once_with(method="get", path="languages/1") 92 | 93 | @mock.patch("crowdin_api.requester.APIRequester.request") 94 | def test_delete_custom_language(self, m_request, base_absolut_url): 95 | m_request.return_value = "response" 96 | 97 | resource = self.get_resource(base_absolut_url) 98 | assert resource.delete_custom_language(languageId=1) == "response" 99 | m_request.assert_called_once_with(method="delete", path="languages/1") 100 | 101 | @mock.patch("crowdin_api.requester.APIRequester.request") 102 | def test_edit_custom_language(self, m_request, base_absolut_url): 103 | m_request.return_value = "response" 104 | 105 | data = [ 106 | { 107 | "value": "test", 108 | "op": PatchOperation.REPLACE, 109 | "path": LanguagesPatchPath.NAME, 110 | } 111 | ] 112 | 113 | resource = self.get_resource(base_absolut_url) 114 | assert resource.edit_custom_language(languageId=1, data=data) == "response" 115 | m_request.assert_called_once_with(method="patch", request_data=data, path="languages/1") 116 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/languages/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.languages.enums import LanguagesPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class LanguagesPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: LanguagesPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/machine_translation_engines/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/machine_translation_engines/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LanguageRecognitionProvider(Enum): 5 | CROWDIN = "crowdin" 6 | ENGINE = "engine" 7 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/machine_translation_engines/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Iterable 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from .enums import LanguageRecognitionProvider 5 | 6 | 7 | class MachineTranslationEnginesResource(BaseResource): 8 | """ 9 | Resource for Machine Translation Engines. 10 | 11 | Machine Translation Engines (MTE) are the sources for pre-translations. 12 | 13 | Use API to add, update, and delete specific MTE. 14 | 15 | Link to documentation: 16 | https://developer.crowdin.com/api/v2/#tag/Machine-Translation-Engines 17 | """ 18 | 19 | def get_mts_path(self, mtId: Optional[int] = None): 20 | if mtId is not None: 21 | return f"mts/{mtId}" 22 | 23 | return "mts" 24 | 25 | def list_mts(self, limit: Optional[int] = None, offset: Optional[int] = None): 26 | """ 27 | List MTs. 28 | 29 | Link to documentation: 30 | https://developer.crowdin.com/api/v2/#operation/api.mts.getMany 31 | """ 32 | 33 | return self._get_entire_data( 34 | method="get", 35 | path=self.get_mts_path(), 36 | params=self.get_page_params(offset=offset, limit=limit), 37 | ) 38 | 39 | def get_mt(self, mtId: int): 40 | """ 41 | Get MT. 42 | 43 | Link to documentation: 44 | https://developer.crowdin.com/api/v2/#operation/api.mts.get 45 | """ 46 | 47 | return self.requester.request(method="get", path=self.get_mts_path(mtId=mtId)) 48 | 49 | def translate_via_mt( 50 | self, 51 | mtId: int, 52 | targetLanguageId: str = None, 53 | languageRecognitionProvider: Optional[LanguageRecognitionProvider] = None, 54 | sourceLanguageId: Optional[str] = None, 55 | strings: Optional[Iterable[str]] = None, 56 | ): 57 | """ 58 | Create Translate via MT. 59 | 60 | Link to documentation: 61 | https://developer.crowdin.com/api/v2/#operation/api.mts.translations.post 62 | """ 63 | return self.requester.request( 64 | method="post", 65 | path=f"{self.get_mts_path(mtId=mtId)}/translations", 66 | request_data={ 67 | "targetLanguageId": targetLanguageId, 68 | "languageRecognitionProvider": languageRecognitionProvider, 69 | "sourceLanguageId": sourceLanguageId, 70 | "strings": strings, 71 | }, 72 | ) 73 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/machine_translation_engines/tests/test_machine_translation_engines_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from crowdin_api.api_resources import MachineTranslationEnginesResource 6 | from crowdin_api.requester import APIRequester 7 | 8 | 9 | class TestTranslationMemoryResource: 10 | resource_class = MachineTranslationEnginesResource 11 | 12 | def get_resource(self, base_absolut_url): 13 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 14 | 15 | @pytest.mark.parametrize( 16 | "input_mtId, path", 17 | ( 18 | (None, "mts"), 19 | (1, "mts/1"), 20 | ), 21 | ) 22 | def test_get_mts_path(self, input_mtId, path, base_absolut_url): 23 | resource = self.get_resource(base_absolut_url) 24 | assert resource.get_mts_path(mtId=input_mtId) == path 25 | 26 | @mock.patch("crowdin_api.requester.APIRequester.request") 27 | def test_list_mts(self, m_request, base_absolut_url): 28 | m_request.return_value = "response" 29 | 30 | resource = self.get_resource(base_absolut_url) 31 | assert resource.list_mts() == "response" 32 | m_request.assert_called_once_with( 33 | method="get", 34 | params={"offset": 0, "limit": 25}, 35 | path=resource.get_mts_path(), 36 | ) 37 | 38 | @mock.patch("crowdin_api.requester.APIRequester.request") 39 | def test_get_tm(self, m_request, base_absolut_url): 40 | m_request.return_value = "response" 41 | 42 | resource = self.get_resource(base_absolut_url) 43 | assert resource.get_mt(mtId=1) == "response" 44 | m_request.assert_called_once_with( 45 | method="get", path=resource.get_mts_path(mtId=1) 46 | ) 47 | 48 | @pytest.mark.parametrize( 49 | "in_params, request_data", 50 | ( 51 | ( 52 | {"mtId": 1, "targetLanguageId": "test"}, 53 | { 54 | "targetLanguageId": "test", 55 | "languageRecognitionProvider": None, 56 | "sourceLanguageId": None, 57 | "strings": None, 58 | }, 59 | ), 60 | ( 61 | { 62 | "mtId": 1, 63 | "targetLanguageId": "test", 64 | "languageRecognitionProvider": "crowdin", 65 | "sourceLanguageId": "test", 66 | "strings": ["test", "test"], 67 | }, 68 | { 69 | "targetLanguageId": "test", 70 | "languageRecognitionProvider": "crowdin", 71 | "sourceLanguageId": "test", 72 | "strings": ["test", "test"], 73 | }, 74 | ), 75 | ), 76 | ) 77 | @mock.patch("crowdin_api.requester.APIRequester.request") 78 | def test_translate_via_mt( 79 | self, m_request, in_params, request_data, base_absolut_url 80 | ): 81 | m_request.return_value = "response" 82 | 83 | resource = self.get_resource(base_absolut_url) 84 | assert resource.translate_via_mt(**in_params) == "response" 85 | m_request.assert_called_once_with( 86 | method="post", 87 | path=resource.get_mts_path(mtId=1) + "/translations", 88 | request_data=request_data, 89 | ) 90 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/notifications/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {"tests": False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/notifications/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MemberRole(Enum): 5 | OWNER = "owner" 6 | MANAGER = "manager" 7 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/notifications/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.notifications.types import ( 5 | ByRoleRequestScehme, 6 | ByUserIdsRequestScheme, 7 | ) 8 | 9 | 10 | class NotificationResource(BaseResource): 11 | """ 12 | Resource for Notifications 13 | 14 | Link to documetation: 15 | https://developer.crowdin.com/api/v2/#tag/Notifications 16 | """ 17 | 18 | def send_notification_to_authenticated_user(self, message: str): 19 | """ 20 | Send Notification to Authenticated User 21 | 22 | Link to documentation: 23 | https://developer.crowdin.com/api/v2/#operation/api.notify.post 24 | """ 25 | return self.requester.request( 26 | method="post", path="notify", request_data={"message": message} 27 | ) 28 | 29 | def send_notification_to_project_members( 30 | self, 31 | request_data: Union[ByUserIdsRequestScheme, ByRoleRequestScehme], 32 | projectId: Optional[int] = None, 33 | ): 34 | """ 35 | Send Notification To Project Members 36 | 37 | Link to documentation: 38 | https://developer.crowdin.com/api/v2/#operation/api.projects.notify.post 39 | 40 | 41 | Link to documentation (Enterprise): 42 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.notify.post 43 | """ 44 | 45 | projectId = projectId or self.get_project_id() 46 | 47 | return self.requester.request( 48 | method="post", 49 | path=f"projects/{projectId}/notify", 50 | request_data=request_data, 51 | ) 52 | 53 | def send_notification_to_organization_members( 54 | self, 55 | request_data: Union[ByUserIdsRequestScheme, ByRoleRequestScehme], 56 | ): 57 | """ 58 | Send Notification To Organization Members 59 | 60 | Link to documentation (Enterprise): 61 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.notify.post 62 | """ 63 | return self.requester.request( 64 | method="post", path="notify", request_data=request_data 65 | ) 66 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/notifications/tests/test_notification_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | 4 | from crowdin_api.requester import APIRequester 5 | from crowdin_api.api_resources.notifications.resource import NotificationResource 6 | from crowdin_api.api_resources.notifications.enums import MemberRole 7 | 8 | 9 | class TestNotificationResource: 10 | resource_class = NotificationResource 11 | 12 | def get_resource(self, base_absolut_url): 13 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 14 | 15 | def test_resource_with_id(self, base_absolut_url): 16 | project_id = 1 17 | resource = self.resource_class( 18 | requester=APIRequester(base_url=base_absolut_url), project_id=project_id 19 | ) 20 | assert resource.get_project_id() == project_id 21 | 22 | @mock.patch("crowdin_api.requester.APIRequester.request") 23 | def test_send_notification_to_authenticated_user(self, m_request, base_absolut_url): 24 | m_request.return_value = "response" 25 | 26 | resource = self.get_resource(base_absolut_url=base_absolut_url) 27 | message = "TEST MESSAGE" 28 | 29 | assert ( 30 | resource.send_notification_to_authenticated_user(message=message) 31 | == "response" 32 | ) 33 | m_request.assert_called_once_with( 34 | method="post", path="notify", request_data={"message": message} 35 | ) 36 | 37 | @pytest.mark.parametrize( 38 | "in_params, request_data", 39 | ( 40 | ( 41 | { 42 | "userIds": [ 43 | 1, 44 | 2, 45 | ], 46 | "message": "TEST MESSAGE", 47 | }, 48 | { 49 | "userIds": [ 50 | 1, 51 | 2, 52 | ], 53 | "message": "TEST MESSAGE", 54 | }, 55 | ), 56 | ( 57 | { 58 | "role": MemberRole.OWNER, 59 | "message": "TEST MESSAGE", 60 | }, 61 | { 62 | "role": MemberRole.OWNER, 63 | "message": "TEST MESSAGE", 64 | }, 65 | ), 66 | ( 67 | { 68 | "role": MemberRole.MANAGER, 69 | "message": "TEST MESSAGE", 70 | }, 71 | { 72 | "role": MemberRole.MANAGER, 73 | "message": "TEST MESSAGE", 74 | }, 75 | ), 76 | ), 77 | ) 78 | @mock.patch("crowdin_api.requester.APIRequester.request") 79 | def test_send_notification_to_project_members( 80 | self, m_request, in_params, request_data, base_absolut_url 81 | ): 82 | m_request.return_value = "response" 83 | 84 | resource = self.get_resource(base_absolut_url=base_absolut_url) 85 | projectId = 1 86 | 87 | assert resource.send_notification_to_project_members( 88 | projectId=projectId, request_data=in_params 89 | ) 90 | m_request.assert_called_once_with( 91 | method="post", 92 | path=f"projects/{projectId}/notify", 93 | request_data=request_data, 94 | ) 95 | 96 | @pytest.mark.parametrize( 97 | "in_params, request_data", 98 | ( 99 | ( 100 | { 101 | "userIds": [ 102 | 1, 103 | 2, 104 | ], 105 | "message": "TEST MESSAGE", 106 | }, 107 | { 108 | "userIds": [ 109 | 1, 110 | 2, 111 | ], 112 | "message": "TEST MESSAGE", 113 | }, 114 | ), 115 | ( 116 | { 117 | "role": MemberRole.OWNER, 118 | "message": "TEST MESSAGE", 119 | }, 120 | { 121 | "role": MemberRole.OWNER, 122 | "message": "TEST MESSAGE", 123 | }, 124 | ), 125 | ( 126 | { 127 | "role": MemberRole.MANAGER, 128 | "message": "TEST MESSAGE", 129 | }, 130 | { 131 | "role": MemberRole.MANAGER, 132 | "message": "TEST MESSAGE", 133 | }, 134 | ), 135 | ), 136 | ) 137 | @mock.patch("crowdin_api.requester.APIRequester.request") 138 | def test_send_notification_to_organization_members( 139 | self, m_request, in_params, request_data, base_absolut_url 140 | ): 141 | m_request.return_value = "response" 142 | 143 | resource = self.get_resource(base_absolut_url=base_absolut_url) 144 | 145 | assert resource.send_notification_to_organization_members(request_data=in_params) 146 | m_request.assert_called_once_with( 147 | method="post", 148 | path="notify", 149 | request_data=request_data, 150 | ) 151 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/notifications/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, Iterable 2 | from crowdin_api.api_resources.notifications.enums import MemberRole 3 | 4 | 5 | class ByRoleRequestScehme(TypedDict): 6 | role: MemberRole 7 | message: str 8 | 9 | 10 | class ByUserIdsRequestScheme(TypedDict): 11 | userIds: Iterable[int] 12 | message: str 13 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/projects/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/projects/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class HasManagerAccess(Enum): 5 | TRUE = 1 6 | FALSE = 0 7 | 8 | 9 | class ProjectType(Enum): 10 | FILE_BASED = 0 11 | STRING_BASED = 1 12 | 13 | 14 | class ProjectVisibility(Enum): 15 | PRIVATE = "private" 16 | OPEN = "open" 17 | 18 | 19 | class ProjectLanguageAccessPolicy(Enum): 20 | OPEN = "open" 21 | MODERATE = "moderate" 22 | 23 | 24 | class ProjectPatchPath(Enum): 25 | NAME = "/name" 26 | TARGET_LANGUAGE_IDS = "/targetLanguageIds" 27 | CNAME = "/cname" 28 | VISIBILITY = "/visibility" 29 | LANGUAGE_ACCESS_POLICY = "/languageAccessPolicy" 30 | DESCRIPTION = "/description" 31 | TRANSLATE_DUPLICATES = "/translateDuplicates" 32 | IS_MT_ALLOWED = "/isMtAllowed" 33 | AUTO_SUBSTITUTION = "/autoSubstitution" 34 | SKIP_UNTRANSLATED_STRINGS = "/skipUntranslatedStrings" 35 | SKIP_UNTRANSLATED_FILES = "/skipUntranslatedFiles" 36 | EXPORT_APPROVED_ONLY = "/exportApprovedOnly" 37 | AUTO_TRANSLATE_DIALECTS = "/autoTranslateDialects" 38 | PUBLIC_DOWNLOADS = "/publicDownloads" 39 | USE_GLOBAL_TM = "/useGlobalTm" 40 | NORMALIZE_PLACEHOLDER = "normalizePlaceholder" 41 | SAVE_META_INFO_IN_SOURCE = "saveMetaInfoInSource" 42 | IN_CONTEXT = "/inContext" 43 | IN_CONTEXT_PSEUDO_LANGUAGE_ID = "/inContextPseudoLanguageId" 44 | QA_CHECK_IS_ACTIVE = "/qaCheckIsActive" 45 | QA_CHECK_CATEGORIES = "/qaCheckCategories" 46 | QA_CHECK_CATEGORY = "/qaCheckCategories/{category}" 47 | QA_CHECKS_IGNORABLE_CATEGORIES = "/qaChecksIgnorableCategories" 48 | QA_CHECKS_IGNORABLE_CATEGORY = "/qaChecksIgnorableCategories/{category}" 49 | LANGUAGE_MAPPING = "/languageMapping" 50 | LANGUAGE_MAPPING_ID = "/languageMapping/{languageId}" 51 | LANGUAGE_MAPPING_KEY = "/languageMapping/{languageId}/{mappingKey}" 52 | DEFAULT_TM_ID = "/defaultTmId" 53 | DEFAULT_GLOSSARY_ID = "/defaultGlossaryId" 54 | 55 | 56 | class ProjectTranslateDuplicates(Enum): 57 | SHOW = 0 58 | HIDE_REGULAR_DETECTION = 1 59 | SHOW_AUTO_TRANSLATE = 2 60 | SHOW_REGULAR_DETECTION = 3 61 | HIDE_STRICT_DETECTION = 4 62 | SHOW_STRICT_DETECTION = 5 63 | 64 | 65 | class EscapeQuotes(Enum): 66 | """ 67 | Values available: 68 | 0 - Do not escape single quote 69 | 1 - Escape single quote by another single quote 70 | 2 - Escape single quote by a backslash 71 | 3 - Escape single quote by another single quote only in strings containing variables 72 | """ 73 | ZERO = 0 74 | ONE = 1 75 | TWO = 2 76 | THREE = 3 77 | 78 | 79 | class EscapeSpecialCharacters(Enum): 80 | """ 81 | Defines whether any special characters (=, :, ! and #) should be escaped by backslash in 82 | exported translations. 83 | You can add escape_special_characters per-file option. * 84 | 85 | Acceptable values are: 86 | 0 - Do not escape special characters 87 | 1 - Escape special characters by a backslash 88 | """ 89 | ZERO = 0 90 | ONE = 1 91 | 92 | 93 | class ProjectFilePatchPath(Enum): 94 | FORMAT = "/format" 95 | SETTINGS = "/settings" 96 | 97 | 98 | class ListProjectsOrderBy(Enum): 99 | ID = "id" 100 | NAME = "name" 101 | IDENTIFIER = "identifier" 102 | DESCRIPTION = "description" 103 | CREATED_AT = "createdAt" 104 | UPDATED_AT = "updatedAt" 105 | LAST_ACTIVITY = "lastActivity" 106 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/projects/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable, List, Union, Dict 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.projects.enums import ( 5 | ProjectPatchPath, 6 | EscapeQuotes, 7 | EscapeSpecialCharacters, 8 | ProjectFilePatchPath, 9 | ) 10 | from crowdin_api.typing import TypedDict 11 | 12 | 13 | class ProjectPatchRequest(TypedDict): 14 | value: Any 15 | op: PatchOperation 16 | path: ProjectPatchPath 17 | 18 | 19 | class NotificationSettings(TypedDict): 20 | translatorNewStrings: bool 21 | managerNewStrings: bool 22 | managerLanguageCompleted: bool 23 | 24 | 25 | class QACheckCategories(TypedDict): 26 | EMPTY: bool 27 | SIZE: bool 28 | TAGS: bool 29 | SPACES: bool 30 | VARIABLES: bool 31 | PUNCTUATION: bool 32 | SYMBOLREGISTER: bool 33 | SPECIALSYMBOLS: bool 34 | WRONGTRANSLATION: bool 35 | SPELLCHECK: bool 36 | ICU: bool 37 | TERMS: bool 38 | DUPLICATE: bool 39 | 40 | 41 | class QAChecksIgnorableCategories(TypedDict): 42 | EMPTY: bool 43 | SIZE: bool 44 | TAGS: bool 45 | SPACES: bool 46 | VARIABLES: bool 47 | PUNCTUATION: bool 48 | SYMBOLREGISTER: bool 49 | SPECIALSYMBOLS: bool 50 | WRONGTRANSLATION: bool 51 | SPELLCHECK: bool 52 | ICU: bool 53 | TERMS: bool 54 | DUPLICATE: bool 55 | FTL: bool 56 | ANDROID: bool 57 | 58 | 59 | class PropertyFileFormatSettings(TypedDict): 60 | escapeQuotes: EscapeQuotes 61 | escapeSpecialCharacters: EscapeSpecialCharacters 62 | exportPattern: str 63 | 64 | 65 | class XmlFileFormatSettings(TypedDict): 66 | translateContent: bool 67 | translateAttributes: bool 68 | translatableElements: Iterable[str] 69 | contentSegmentation: bool 70 | srxStorageId: int 71 | exportPattern: str 72 | 73 | 74 | class SpecificFileFormatSettings(TypedDict): 75 | """ 76 | Includes kind standard file format settings: 77 | - WebXml file 78 | - Html file 79 | - Adoc file 80 | - Md file 81 | - FmMd file 82 | - FmHtml file 83 | - MadcapFisnp file 84 | - Idml file 85 | - Mif file 86 | - Dita file 87 | """ 88 | contentSegmentation: bool 89 | srxStorageId: int 90 | exportPattern: str 91 | 92 | 93 | class DocxFileFormatSettings(TypedDict): 94 | cleanTagsAggressively: bool 95 | translateHiddenText: bool 96 | translateHyperlinkUrls: bool 97 | translateHiddenRowsAndColumns: bool 98 | importNotes: bool 99 | importHiddenSlides: bool 100 | contentSegmentation: bool 101 | srxStorageId: int 102 | exportPattern: str 103 | 104 | 105 | class MediaWikiFileFormatSettings(TypedDict): 106 | srxStorageId: int 107 | exportPattern: str 108 | 109 | 110 | class TxtFileFormatSettings(TypedDict): 111 | srxStorageId: int 112 | exportPattern: str 113 | 114 | 115 | class OtherFileFormatSettings(TypedDict): 116 | exportPattern: str 117 | 118 | 119 | class AndroidStringsExporterSettings(TypedDict): 120 | convertPlaceholders: bool 121 | 122 | 123 | class MacOSXStringsExporterSettings(TypedDict): 124 | convertPlaceholders: bool 125 | 126 | 127 | class XliffStringsExporterSettings(TypedDict): 128 | languagePairMapping: Dict[str, str] 129 | 130 | 131 | class ProjectFilePatchRequest(TypedDict): 132 | value: Union[str, List[str]] 133 | op: PatchOperation 134 | path: ProjectFilePatchPath 135 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ExportFormat(Enum): 5 | XLSX = "xlsx" 6 | CSV = "csv" 7 | JSON = "json" 8 | 9 | 10 | class ScopeType(Enum): 11 | ORGANIZATION = "organization" 12 | GRPOUP = "group" 13 | PROJECT = "project" 14 | 15 | 16 | class ReportName(Enum): 17 | PRE_TRANSLATE_ACCURACY = "pre-translate-accuracy" 18 | COSTS_ESTIMATION = "costs-estimation" 19 | TRANSLATION_COSTS = "translation-costs" 20 | TOP_MEMBERS = "top-members" 21 | CONTRIBUTION_RAW_DATA = "contribution-raw-data" 22 | COSTS_ESTIMATION_POST_EDITING = "costs-estimation-pe" 23 | TRANSLATION_COSTS_POST_EDITING = "translation-costs-pe" 24 | 25 | 26 | class Unit(Enum): 27 | STRINGS = "strings" 28 | WORDS = "words" 29 | CHARS = "chars" 30 | CHARS_WITH_SPACES = "chars_with_spaces" 31 | 32 | 33 | class Currency(Enum): 34 | USD = "USD" 35 | EUR = "EUR" 36 | JPY = "JPY" 37 | GBP = "GBP" 38 | AUD = "AUD" 39 | CAD = "CAD" 40 | CHF = "CHF" 41 | CNY = "CNY" 42 | SEK = "SEK" 43 | NZD = "NZD" 44 | MXN = "MXN" 45 | SGD = "SGD" 46 | HKD = "HKD" 47 | NOK = "NOK" 48 | KRW = "KRW" 49 | TRY = "TRY" 50 | RUB = "RUB" 51 | INR = "INR" 52 | BRL = "BRL" 53 | ZAR = "ZAR" 54 | GEL = "GEL" 55 | UAH = "UAH" 56 | 57 | 58 | class SchemaMode(Enum): 59 | SIMPLE = "simple" 60 | FUZZY = "fuzzy" 61 | 62 | 63 | class Format(Enum): 64 | XLSX = "xlsx" 65 | CSV = "csv" 66 | JSON = "json" 67 | 68 | 69 | class SimpleRateMode(Enum): 70 | NO_MATCH = "no_match" 71 | TM_MATCH = "tm_match" 72 | APPROVAL = "approval" 73 | 74 | 75 | class FuzzyRateMode(Enum): 76 | NO_MATCH = "no_match" 77 | PERFECT = "perfect" 78 | ONE_HUNDRED = "100" 79 | MORE_NINETY_FIVE = "99-95" 80 | MORE_NINETY = "94-90" 81 | MORE_EIGHTY = "89-80" 82 | APPROVAL = "approval" 83 | 84 | 85 | class GroupBy(Enum): 86 | USER = "user" 87 | LANGUAGE = "language" 88 | 89 | 90 | class ContributionMode: 91 | TRANSLATIONS = "translations" 92 | APPROVALS = "approvals" 93 | VOTES = "votes" 94 | 95 | 96 | class ReportSettingsTemplatesPatchPath(Enum): 97 | NAME = "name" 98 | CURRENCY = "currency" 99 | UNIT = "unit" 100 | MODE = "mode" 101 | CONFIG = "config" 102 | 103 | 104 | class ReportLabelIncludeType(Enum): 105 | STRINGS_WITH_LABEL = "strings_with_label" 106 | STRINGS_WITHOUT_LABEL = "strings_without_label" 107 | 108 | 109 | class MatchType(Enum): 110 | PERFECT = "perfect" 111 | OPTION_100 = "100" 112 | OPTION_99_82 = "99-82" 113 | OPTION_81_60 = "81-60" 114 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/requests/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/requests/cost_estimation_post_editing.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from crowdin_api.api_resources.reports.types import Match 4 | from crowdin_api.typing import TypedDict 5 | 6 | 7 | class IndividualRate(TypedDict): 8 | languageIds: Iterable[str] 9 | userIds: Iterable[int] 10 | fullTranslation: float 11 | proofread: float 12 | 13 | 14 | class NetRateSchemes(TypedDict): 15 | tmMatch: Iterable[Match] 16 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/requests/group_translation_costs_post_editing.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from crowdin_api.api_resources.reports.types import Match 4 | from crowdin_api.typing import TypedDict 5 | 6 | 7 | class IndividualRate(TypedDict): 8 | languageIds: Iterable[str] 9 | userIds: Iterable[int] 10 | fullTranslation: float 11 | proofread: float 12 | 13 | 14 | class NetRateSchemes(TypedDict): 15 | tmMatch: Iterable[Match] 16 | mtMatch: Iterable[Match] 17 | suggestionMatch: Iterable[Match] 18 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/requests/translation_costs_post_editing.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from crowdin_api.api_resources.reports.types import Match 3 | from crowdin_api.typing import TypedDict 4 | 5 | 6 | class IndividualRate(TypedDict): 7 | languageIds: Iterable[str] 8 | userIds: Iterable[int] 9 | fullTranslation: float 10 | proofread: float 11 | 12 | 13 | class NetRateSchemes(TypedDict): 14 | tmMatch: Iterable[Match] 15 | mtMatch: Iterable[Match] 16 | suggestionMatch: Iterable[Match] 17 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/reports/types.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.reports.enums import ( 5 | FuzzyRateMode, 6 | SimpleRateMode, 7 | ReportSettingsTemplatesPatchPath, 8 | MatchType 9 | ) 10 | from crowdin_api.typing import TypedDict 11 | 12 | 13 | class SimpleRegularRate(TypedDict): 14 | mode: SimpleRateMode 15 | value: Union[float, int] 16 | 17 | 18 | class SimpleIndividualRate(TypedDict): 19 | languageIds: Iterable[str] 20 | rates: Iterable[SimpleRegularRate] 21 | 22 | 23 | class SimpleSettingsTemplateRate(SimpleIndividualRate): 24 | userIds: Iterable[int] 25 | 26 | 27 | class FuzzyRegularRate(TypedDict): 28 | mode: FuzzyRateMode 29 | value: Union[float, int] 30 | 31 | 32 | class FuzzyIndividualRate(TypedDict): 33 | languageIds: Iterable[str] 34 | rates: Iterable[FuzzyRegularRate] 35 | 36 | 37 | class TranslateStep(TypedDict): 38 | regularRates: Iterable[FuzzyRegularRate] 39 | individualRates: Iterable[FuzzyIndividualRate] 40 | 41 | 42 | class StepTypes(TypedDict): 43 | stepTypes: Iterable[TranslateStep] 44 | 45 | 46 | class Config(TypedDict): 47 | regularRates: Iterable[SimpleRegularRate] 48 | individualRates: Iterable[SimpleSettingsTemplateRate] 49 | 50 | 51 | class ReportSettingsTemplatesPatchRequest(TypedDict): 52 | value: Union[str, int] 53 | op: PatchOperation 54 | path: ReportSettingsTemplatesPatchPath 55 | 56 | 57 | class Match(TypedDict): 58 | matchType: MatchType 59 | price: float 60 | 61 | 62 | class BaseRates(TypedDict): 63 | fullTranslation: float 64 | proofread: float 65 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/screenshots/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/screenshots/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ScreenshotPatchPath(Enum): 5 | NAME = "/name" 6 | 7 | 8 | class TagPatchPath(Enum): 9 | STRING_ID = "/stringId" 10 | POSITION = "/position" 11 | 12 | 13 | class ListScreenshotsOrderBy(Enum): 14 | ID = "id" 15 | NAME = "name" 16 | TAGS_COUNT = "tagsCount" 17 | CREATED_AT = "createdAt" 18 | UPDATED_AT = "updatedAt" 19 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/screenshots/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Union 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.screenshots.enums import ScreenshotPatchPath, TagPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class ScreenshotPatchRequest(TypedDict): 9 | value: str 10 | op: Union[PatchOperation, str] 11 | path: ScreenshotPatchPath 12 | 13 | 14 | class Position(TypedDict): 15 | x: int 16 | y: int 17 | width: int 18 | height: int 19 | 20 | 21 | class AddTagRequest(TypedDict): 22 | stringId: int 23 | position: Optional[Position] 24 | 25 | 26 | class TagPatchRequest(TypedDict): 27 | value: Any 28 | op: Union[PatchOperation, str] 29 | path: TagPatchPath 30 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/security_logs/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {"tests": False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/security_logs/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class SecurityLogEvent(Enum): 5 | LOGIN = "login" 6 | PASSWORD_SET = "password.set" 7 | PASSWORD_CHANGE = "password.change" 8 | EMAIL_CHANGE = "email.change" 9 | LOGIN_CHANGE = "login.change" 10 | PERSONAL_TOKEN_ISSUED = "personal_token.issued" 11 | PERSONAL_TOKEN_REVOKED = "personal_token.revoked" 12 | MFA_ENABLED = "mfa.enabled" 13 | MFA_DISABLED = "mfa.disabled" 14 | SESSION_REVOKE = "session.revoke" 15 | SESSION_REVOKE_ALL = "session.revoke_all" 16 | SSO_CONNECT = "sso.connect" 17 | SSO_DISCONNECT = "sso.disconnect" 18 | USER_REMOVE = "user.remove" 19 | APPLICATION_CONNECTED = "application.connected" 20 | APPLICATION_DISCONNECTED = "application.disconnected" 21 | WEBAUTHN_CREATED = "webauthn.created" 22 | WEBAUTHN_DELETED = "webauthn.deleted" 23 | TRUSTED_DEVICE_REMOVE = "trusted_device.remove" 24 | TRUSTED_DEVICE_REMOVE_ALL = "trusted_device.remove_all" 25 | DEVICE_VERIFICATION_ENABLED = "device_verification.enabled" 26 | DEVICE_VERIFICATION_DISABLED = "device_verification.disabled" 27 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/security_logs/resource.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from crowdin_api.api_resources.abstract.resources import BaseResource 5 | from crowdin_api.api_resources.security_logs.enums import SecurityLogEvent 6 | 7 | 8 | class SecurityLogsResource(BaseResource): 9 | """ 10 | Resource for Security Logs 11 | 12 | Link to documentaion: 13 | https://developer.crowdin.com/api/v2/#tag/Security-Logs 14 | 15 | Link to documentation for enterprise: 16 | https://developer.crowdin.com/enterprise/api/v2/#tag/Security-Logs 17 | """ 18 | 19 | def get_user_security_logs_path( 20 | self, userId: int, securityLogId: Optional[int] = None 21 | ): 22 | if securityLogId is not None: 23 | return f"/users/{userId}/security-logs/{securityLogId}" 24 | return f"/users/{userId}/security-logs" 25 | 26 | def list_user_security_logs( 27 | self, 28 | userId: int, 29 | event: Optional[SecurityLogEvent] = None, 30 | createdAfter: Optional[datetime] = None, 31 | createdBefore: Optional[datetime] = None, 32 | ipAddress: Optional[str] = None, 33 | limit: Optional[int] = None, 34 | offset: Optional[int] = None, 35 | page: Optional[int] = None, 36 | ): 37 | """ 38 | List User Security Logs 39 | 40 | Link to documentaion: 41 | https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.getMany 42 | """ 43 | 44 | params = { 45 | "event": event, 46 | "createdAfter": createdAfter, 47 | "createdBefore": createdBefore, 48 | "ipAddress": ipAddress, 49 | } 50 | params.update(self.get_page_params(page=page, offset=offset, limit=limit)) 51 | 52 | return self._get_entire_data( 53 | method="get", 54 | path=self.get_user_security_logs_path(userId=userId), 55 | params=params, 56 | ) 57 | 58 | def get_user_security_log(self, userId: int, securityLogId: int): 59 | """ 60 | Get User Security Log 61 | 62 | Link to documentation: 63 | https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.get 64 | """ 65 | 66 | return self.requester.request( 67 | method="get", 68 | path=self.get_user_security_logs_path( 69 | userId=userId, securityLogId=securityLogId 70 | ), 71 | ) 72 | 73 | def get_organization_security_logs_path(self, securityLogId: Optional[int] = None): 74 | if securityLogId is not None: 75 | return f"/security-logs/{securityLogId}" 76 | return "/security-logs" 77 | 78 | def list_organization_security_logs( 79 | self, 80 | event: Optional[SecurityLogEvent] = None, 81 | createdAfter: Optional[datetime] = None, 82 | createdBefore: Optional[datetime] = None, 83 | ipAddress: Optional[str] = None, 84 | userId: Optional[int] = None, 85 | limit: Optional[int] = None, 86 | offset: Optional[int] = None, 87 | page: Optional[int] = None, 88 | ): 89 | """ 90 | List Organization Security Logs 91 | 92 | Link to documentation: 93 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.getMany 94 | """ 95 | 96 | params = { 97 | "event": event, 98 | "createdAfter": createdAfter, 99 | "createdBefore": createdBefore, 100 | "ipAddress": ipAddress, 101 | "userId": userId, 102 | } 103 | params.update(self.get_page_params(page=page, offset=offset, limit=limit)) 104 | 105 | return self._get_entire_data( 106 | method="get", 107 | path=self.get_organization_security_logs_path(), 108 | params=params, 109 | ) 110 | 111 | def get_organization_security_log(self, securityLogId: int): 112 | """ 113 | Get Organization Security Log 114 | 115 | Link to documentaion: 116 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.get 117 | """ 118 | 119 | return self.requester.request( 120 | method="get", 121 | path=self.get_organization_security_logs_path(securityLogId=securityLogId), 122 | ) 123 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_files/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_files/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Priority(Enum): 5 | LOW = "low" 6 | NORMAL = "normal" 7 | HIGH = "high" 8 | 9 | 10 | class BranchPatchPath(Enum): 11 | NAME = "/name" 12 | TITLE = "/title" 13 | EXPORT_PATTERN = "/exportPattern" 14 | PRIORITY = "/priority" 15 | 16 | 17 | class DirectoryPatchPath(Enum): 18 | BRANCH_ID = "/branchId" 19 | DIRECTORY_ID = "/directoryId" 20 | NAME = "/name" 21 | TITLE = "/title" 22 | EXPORT_PATTERN = "/exportPattern" 23 | PRIORITY = "/priority" 24 | 25 | 26 | class FileType(Enum): 27 | AUTO = "auto" 28 | ANDROID = "android" 29 | MACOSX = "macosx" 30 | RESX = "resx" 31 | PROPERTIES = "properties" 32 | GETTEXT = "gettext" 33 | YAML = "yaml" 34 | PHP = "php" 35 | JSON = "json" 36 | XML = "xml" 37 | INI = "ini" 38 | RC = "rc" 39 | RESW = "resw" 40 | RESJSON = "resjson" 41 | QTTS = "qtts" 42 | JOOMLA = "joomla" 43 | CHROME = "chrome" 44 | DTD = "dtd" 45 | DKLANG = "dklang" 46 | FLEX = "flex" 47 | NSH = "nsh" 48 | WXL = "wxl" 49 | XLIFF = "xliff" 50 | HTML = "html" 51 | HAML = "haml" 52 | TXT = "txt" 53 | CSV = "csv" 54 | MD = "md" 55 | FLSNP = "flsnp" 56 | FM_HTML = "fm_html" 57 | FM_MD = "fm_md" 58 | MEDIAWIKI = "mediawiki" 59 | DOCX = "docx" 60 | SBV = "sbv" 61 | VTT = "vtt" 62 | SRT = "srt" 63 | 64 | 65 | class EscapeQuotes(Enum): 66 | """ 67 | Values available: 68 | 0 - Do not escape single quote 69 | 1 - Escape single quote by another single quote 70 | 2 - Escape single quote by a backslash 71 | 3 - Escape single quote by another single quote only in strings containing variables 72 | """ 73 | ZERO = 0 74 | ONE = 1 75 | TWO = 2 76 | THREE = 3 77 | 78 | 79 | class ExportQuotes(Enum): 80 | SINGLE = "single" 81 | DOUBLE = "double" 82 | 83 | 84 | class FileUpdateOption(Enum): 85 | CLEAR_TRANSLATIONS_AND_APPROVALS = "clear_translations_and_approvals" 86 | KEEP_TRANSLATIONS = "keep_translations" 87 | KEEP_TRANSLATIONS_AND_APPROVALS = "keep_translations_and_approvals" 88 | 89 | 90 | class FilePatchPath(Enum): 91 | BRANCH_ID = "/branchId" 92 | DIRECTORY_ID = "/directoryId" 93 | NAME = "/name" 94 | TITLE = "/title" 95 | CONTEXT = "/context" 96 | PRIORITY = "/priority" 97 | IMPORT_OPTIONS_CLEAN_TAGS_AGGRESSIVELY = "/importOptions/cleanTagsAggressively" 98 | IMPORT_OPTIONS_TRANSLATE_HIDDEN_TEXT = "/importOptions/translateHiddenText" 99 | IMPORT_OPTIONS_TRANSLATE_HYPERLINK_URLS = "/importOptions/translateHyperlinkUrls" 100 | IMPORT_OPTIONS_TRANSLATE_HIDDEN_ROWS_AND_COLUMNS = ( 101 | "/importOptions/translateHiddenRowsAndColumns" 102 | ) 103 | IMPORT_OPTIONS_IMPORT_NOTES = "/importOptions/importNotes" 104 | IMPORT_OPTIONS_IMPORT_HIDDEN_SLIDES = "/importOptions/importHiddenSlides" 105 | IMPORT_OPTIONS_FIRST_LINE_CONTAINS_HEADER = "/importOptions/firstLineContainsHeader" 106 | IMPORT_OPTIONS_IMPORT_TRANSLATIONS = "/importOptions/importTranslations" 107 | IMPORT_OPTIONS_SCHEME = "/importOptions/scheme" 108 | IMPORT_OPTIONS_TRANSLATE_CONTENT = "/importOptions/translateContent" 109 | IMPORT_OPTIONS_TRANSLATE_ATTRIBUTES = "/importOptions/translateAttributes" 110 | IMPORT_OPTIONS_CONTENT_SEGMENTATION = "/importOptions/contentSegmentation" 111 | IMPORT_OPTIONS_TRANSLATABLE_ELEMENTS = "/importOptions/translatableElements" 112 | IMPORT_OPTIONS_SRX_STORAGE_ID = "/importOptions/srxStorageId" 113 | IMPORT_OPTIONS_CUSTOM_SEGMENTATION = "/importOptions/customSegmentation" 114 | EXPORT_OPTIONS_EXPORT_PATTERN = "/exportOptions/exportPattern" 115 | EXPORT_OPTIONS_ESCAPE_QUOTES = "/exportOptions/escapeQuotes" 116 | EXCLUDED_TARGET_LANGUAGES = "/excludedTargetLanguages" 117 | ATTACH_LABEL_IDS = "/attachLabelIds" 118 | DETACH_LABEL_IDS = "/detachLabelIds" 119 | 120 | 121 | class ListProjectBranchesOrderBy(Enum): 122 | ID = "id" 123 | NAME = "name" 124 | TITLE = "title" 125 | CREATED_AT = "createdAt" 126 | UPDATED_AT = "updatedAt" 127 | EXPORT_PATTERN = "exportPattern" 128 | PRIORITY = "priority" 129 | 130 | 131 | class ListDirectoriesOrderBy(Enum): 132 | ID = "id" 133 | NAME = "name" 134 | TITLE = "title" 135 | CREATED_AT = "createdAt" 136 | UPDATED_AT = "updatedAt" 137 | EXPORT_PATTERN = "exportPattern" 138 | PRIORITY = "priority" 139 | 140 | 141 | class ListFilesOrderBy(Enum): 142 | ID = "id" 143 | NAME = "name" 144 | TITLE = "title" 145 | STATUS = "status" 146 | CREATED_AT = "createdAt" 147 | UPDATED_AT = "updatedAt" 148 | EXPORT_PATTERN = "exportPattern" 149 | PRIORITY = "priority" 150 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_files/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.source_files.enums import ( 5 | BranchPatchPath, 6 | DirectoryPatchPath, 7 | EscapeQuotes, 8 | ExportQuotes, 9 | FilePatchPath, 10 | ) 11 | from crowdin_api.typing import TypedDict 12 | 13 | 14 | class BranchPatchRequest(TypedDict): 15 | value: Any 16 | op: PatchOperation 17 | path: BranchPatchPath 18 | 19 | 20 | class DirectoryPatchRequest(TypedDict): 21 | value: Any 22 | op: PatchOperation 23 | path: DirectoryPatchPath 24 | 25 | 26 | class Scheme(TypedDict, total=False): 27 | identifier: int 28 | sourcePhrase: int 29 | 30 | 31 | class SpreadsheetImportOptions(TypedDict): 32 | firstLineContainsHeader: bool 33 | importTranslations: bool 34 | scheme: Scheme 35 | srxStorageId: int 36 | 37 | 38 | class XmlImportOptions(TypedDict): 39 | translateContent: bool 40 | translateAttributes: bool 41 | contentSegmentation: bool 42 | translatableElements: Iterable[str] 43 | 44 | 45 | class DocxFileImportOptions(TypedDict): 46 | cleanTagsAggressively: bool 47 | translateHiddenText: bool 48 | translateHyperlinkUrls: bool 49 | translateHiddenRowsAndColumns: bool 50 | importNotes: bool 51 | importHiddenSlides: bool 52 | contentSegmentation: bool 53 | srxStorageId: int 54 | 55 | 56 | class OtherImportOptions(TypedDict): 57 | contentSegmentation: bool 58 | srxStorageId: int 59 | 60 | 61 | class GeneralExportOptions(TypedDict): 62 | exportPattern: str 63 | 64 | 65 | class PropertyExportOptions: 66 | escapeQuotes: EscapeQuotes 67 | exportPattern: str 68 | 69 | 70 | class JavascriptExportOptions: 71 | exportQuotes: ExportQuotes 72 | exportPattern: str 73 | 74 | 75 | class FilePatchRequest(TypedDict): 76 | value: Any 77 | op: PatchOperation 78 | path: FilePatchPath 79 | 80 | 81 | class HtmlFileImportOptions(TypedDict): 82 | excludedElements: Iterable[str] 83 | contentSegmentation: bool 84 | srxStorageId: int 85 | 86 | 87 | class HtmlWithFrontMatterFileImportOptions(HtmlFileImportOptions): 88 | excludedFrontMatterElements: Iterable[str] 89 | 90 | 91 | class MdxV1FileImportOptions(TypedDict): 92 | excludedFrontMatterElements: Iterable[str] 93 | excludeCodeBlocks: bool 94 | contentSegmentation: bool 95 | srxStorageId: int 96 | 97 | 98 | class MdxV2FileImportOptions(TypedDict): 99 | excludedFrontMatterElements: Iterable[str] 100 | excludeCodeBlocks: bool 101 | contentSegmentation: bool 102 | srxStorageId: int 103 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_strings/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_strings/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ScopeFilter(Enum): 5 | IDENTIFIER = "identifier" 6 | TEXT = "text" 7 | CONTEXT = "context" 8 | 9 | 10 | class SourceStringsPatchPath(Enum): 11 | TEXT = "/text" 12 | CONTEXT = "/context" 13 | IS_HIDDEN = "/isHidden" 14 | MAXLENGTH = "/maxLength" 15 | LABEL_IDS = "/labelIds" 16 | 17 | 18 | class StringBatchOperations(Enum): 19 | REPLACE = "replace" 20 | REMOVE = "remove" 21 | ADD = "add" 22 | 23 | 24 | class StringBatchOperationsPath(Enum): 25 | IDENTIFIER = "/{stringId}/identifier" 26 | TEXT = "/{stringId}/text" 27 | CONTEXT = "/{stringId}/context" 28 | IS_HIDDEN = "/{stringId}/isHidden" 29 | MAX_LENGTH = "/{stringId}/maxLength" 30 | LABEL_IDS = "/{stringId}/labelIds" 31 | 32 | 33 | class ListStringsOrderBy(Enum): 34 | ID = "id" 35 | TEXT = "text" 36 | IDENTIFIER = "identifier" 37 | CONTEXT = "context" 38 | CREATED_AT = "createdAt" 39 | UPDATED_AT = "updatedAt" 40 | TYPE = "type" 41 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/source_strings/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.source_strings.enums import ( 5 | SourceStringsPatchPath, 6 | StringBatchOperations, 7 | StringBatchOperationsPath, 8 | ) 9 | from crowdin_api.typing import TypedDict 10 | 11 | 12 | class SourceStringsPatchRequest(TypedDict): 13 | value: Any 14 | op: PatchOperation 15 | path: SourceStringsPatchPath 16 | 17 | 18 | class StringBatchOperationPatchRequest(TypedDict): 19 | op: StringBatchOperations 20 | path: StringBatchOperationsPath 21 | value: Union[str, dict, int, bool] 22 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/storages/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/storages/resource.py: -------------------------------------------------------------------------------- 1 | from typing import IO, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | 5 | 6 | class StoragesResource(BaseResource): 7 | """ 8 | Resource for Storages. 9 | 10 | Storage is a separate container for each file. You need to use Add Storage method before adding 11 | files to your projects via API. Files that should be uploaded into storage include files for 12 | localization, screenshots, Glossaries, and Translation Memories. 13 | 14 | Storage id is the identifier of the file uploaded to the Storage. 15 | 16 | Note: Storage is periodically cleared. The files that were already uploaded to your account will 17 | be removed from storage and will remain in your account. 18 | 19 | Link to documentation: 20 | https://developer.crowdin.com/api/v2/#tag/Storage 21 | """ 22 | 23 | def get_storages_path(self, storageId: Optional[int] = None): 24 | if storageId: 25 | return f"storages/{storageId}" 26 | 27 | return "storages" 28 | 29 | def list_storages( 30 | self, 31 | page: Optional[int] = None, 32 | offset: Optional[int] = None, 33 | limit: Optional[int] = None, 34 | ): 35 | """List Storages. 36 | 37 | Link to documentation: 38 | https://developer.crowdin.com/api/v2/#operation/api.storages.getMany 39 | """ 40 | 41 | return self._get_entire_data( 42 | method="get", 43 | path=self.get_storages_path(), 44 | params=self.get_page_params(page=page, offset=offset, limit=limit), 45 | ) 46 | 47 | def add_storage(self, file: IO): 48 | """Add Storage. 49 | 50 | Link to documentation: 51 | https://developer.crowdin.com/api/v2/#operation/api.storages.post 52 | """ 53 | 54 | return self.requester.request(method="post", path=self.get_storages_path(), file=file) 55 | 56 | def get_storage(self, storageId: int): 57 | """Get Storage. 58 | 59 | Link to documentation: 60 | https://developer.crowdin.com/api/v2/#operation/api.storages.get 61 | """ 62 | 63 | return self.requester.request( 64 | method="get", path=self.get_storages_path(storageId=storageId) 65 | ) 66 | 67 | def delete_storage(self, storageId: int): 68 | """Delete Storage. 69 | 70 | Link to documentation: 71 | https://developer.crowdin.com/api/v2/#operation/api.storages.delete 72 | """ 73 | 74 | return self.requester.request( 75 | method="delete", path=self.get_storages_path(storageId=storageId) 76 | ) 77 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/storages/tests/test_storages_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from crowdin_api.api_resources import StoragesResource 4 | from crowdin_api.requester import APIRequester 5 | 6 | 7 | class TestStoragesResource: 8 | resource_class = StoragesResource 9 | 10 | def get_resource(self, base_absolut_url): 11 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 12 | 13 | @mock.patch("crowdin_api.requester.APIRequester.request") 14 | def test_list_storages(self, m_request, base_absolut_url): 15 | m_request.return_value = "response" 16 | 17 | resource = self.get_resource(base_absolut_url) 18 | assert resource.list_storages(page=10) == "response" 19 | m_request.assert_called_once_with( 20 | method="get", 21 | params=resource.get_page_params(page=10, offset=None, limit=None), 22 | path="storages", 23 | ) 24 | 25 | @mock.patch("crowdin_api.requester.APIRequester.request") 26 | def test_add_storage(self, m_request, base_absolut_url): 27 | m_request.return_value = "response" 28 | 29 | resource = self.get_resource(base_absolut_url) 30 | assert resource.add_storage("SOME_FILE") == "response" 31 | m_request.assert_called_once_with(method="post", path="storages", file="SOME_FILE") 32 | 33 | @mock.patch("crowdin_api.requester.APIRequester.request") 34 | def test_get_storage(self, m_request, base_absolut_url): 35 | m_request.return_value = "response" 36 | 37 | resource = self.get_resource(base_absolut_url) 38 | assert resource.get_storage(storageId=1) == "response" 39 | m_request.assert_called_once_with(method="get", path="storages/1") 40 | 41 | @mock.patch("crowdin_api.requester.APIRequester.request") 42 | def test_delete_storage(self, m_request, base_absolut_url): 43 | m_request.return_value = "response" 44 | 45 | resource = self.get_resource(base_absolut_url) 46 | assert resource.delete_storage(storageId=1) == "response" 47 | m_request.assert_called_once_with(method="delete", path="storages/1") 48 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_comments/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_comments/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class StringCommentType(Enum): 5 | COMMENT = "comment" 6 | ISSUE = "issue" 7 | 8 | 9 | class StringCommentIssueType(Enum): 10 | GENERAL_QUESTION = "general_question" 11 | TRANSLATION_MISTAKE = "translation_mistake" 12 | CONTEXT_REQUEST = "context_request" 13 | SOURCE_MISTAKE = "source_mistake" 14 | 15 | 16 | class StringCommentIssueStatus(Enum): 17 | RESOLVED = "resolved" 18 | UNRESOLVED = "unresolved" 19 | 20 | 21 | class StringCommentPatchPath(Enum): 22 | TEXT = "/text" 23 | ISSUE_STATUS = "/issueStatus" 24 | 25 | 26 | class ListStringCommentsOrderBy(Enum): 27 | ID = "id" 28 | TEXT = "text" 29 | TYPE = "type" 30 | CREATED_AT = "createdAt" 31 | RESOLVED_AT = "resolvedAt" 32 | ISSUE_STATUS = "issueStatus" 33 | ISSUE_TYPE = "issueType" 34 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_comments/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.string_comments.enums import StringCommentPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class StringCommentPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: StringCommentPatchPath 12 | 13 | 14 | class StringCommentBatchOpPatchRequest(TypedDict): 15 | op: PatchOperation 16 | path: str 17 | value: Any 18 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_translations/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_translations/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PreTranslationApplyMethod(Enum): 5 | TM = "tm" 6 | MT = "mt" 7 | 8 | 9 | class VoteMark(Enum): 10 | UP = "up" 11 | DOWN = "down" 12 | 13 | 14 | class ListTranslationApprovalsOrderBy(Enum): 15 | ID = "id" 16 | CREATED_AT = "createdAt" 17 | 18 | 19 | class ListLanguageTranslationsOrderBy(Enum): 20 | TEXT = "text" 21 | STRING_ID = "stringId" 22 | TRANSLATION_ID = "translationId" 23 | CREATED_AT = "createdAt" 24 | 25 | 26 | class ListStringTranslationsOrderBy(Enum): 27 | ID = "id" 28 | TEXT = "text" 29 | RATING = "rating" 30 | CREATED_AT = "createdAt" 31 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/string_translations/types.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | 5 | 6 | class ApprovalBatchOpPatchRequest(TypedDict): 7 | op: PatchOperation 8 | path: str 9 | value: Any 10 | 11 | 12 | class TranslationBatchOpPatchRequest(TypedDict): 13 | op: PatchOperation 14 | path: str 15 | value: Any 16 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/tasks/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/tasks/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable, Union, Optional 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.tasks.enums import ( 5 | TaskOperationPatchPath, 6 | VendorTaskOperationPatchPath, 7 | ConfigTaskOperationPatchPath, 8 | ) 9 | from crowdin_api.typing import TypedDict 10 | 11 | 12 | class CrowdinTaskAssignee(TypedDict, total=False): 13 | id: int 14 | wordsCount: int 15 | 16 | 17 | class TaskPatchRequest(TypedDict): 18 | value: Any 19 | op: PatchOperation 20 | path: TaskOperationPatchPath 21 | 22 | 23 | class VendorPatchRequest(TypedDict): 24 | value: Any 25 | op: PatchOperation 26 | path: VendorTaskOperationPatchPath 27 | 28 | 29 | class ConfigPatchRequest(TypedDict): 30 | value: Union[str, int] 31 | op: PatchOperation 32 | path: ConfigTaskOperationPatchPath 33 | 34 | 35 | class TaskSettingsTemplateConfigLanguage(TypedDict): 36 | languageId: str 37 | userIds: Optional[Iterable[int]] 38 | 39 | 40 | class TaskSettingsTemplateLanguages(TypedDict): 41 | languages: Iterable[TaskSettingsTemplateConfigLanguage] 42 | 43 | 44 | class EnterpriseTaskAssignedTeams(TypedDict, total=False): 45 | id: int 46 | wordsCount: int 47 | 48 | 49 | class EnterpriseTaskSettingsTemplateConfigLanguage(TypedDict): 50 | languageId: str 51 | userIds: Optional[Iterable[int]] 52 | teamIds: Optional[Iterable[int]] 53 | 54 | 55 | class EnterpriseTaskSettingsTemplateLanguages(TypedDict): 56 | languages: Iterable[EnterpriseTaskSettingsTemplateConfigLanguage] 57 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/teams/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/teams/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TeamPatchPath(Enum): 5 | NAME = "/name" 6 | 7 | 8 | class TeamRole(Enum): 9 | TRANSLATOR = "translator" 10 | PROOFREADER = "proofreader" 11 | 12 | 13 | class ListTeamsOrderBy(Enum): 14 | ID = "id" 15 | NAME = "name" 16 | CREATED_AT = "createdAt" 17 | UPDATED_AT = "updatedAt" 18 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/teams/types.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Union, Optional, Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.teams.enums import TeamPatchPath, TeamRole 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class TeamPatchRequest(TypedDict): 9 | value: Union[str, bool, Iterable[int], Iterable[dict]] 10 | op: PatchOperation 11 | path: TeamPatchPath 12 | 13 | 14 | class WorkflowStepId(TypedDict): 15 | workflowStepIds: Union[str, Iterable[int]] 16 | 17 | 18 | class Permissions(TypedDict): 19 | it: WorkflowStepId 20 | de: WorkflowStepId 21 | 22 | 23 | class LanguageData(TypedDict): 24 | allContent: bool 25 | workflowStepIds: Optional[Iterable[Any]] 26 | 27 | 28 | class LanguagesAccessData(TypedDict): 29 | it: LanguageData 30 | uk: LanguageData 31 | 32 | 33 | class RolePermission(TypedDict): 34 | allLanguages: bool 35 | languagesAccess: Optional[LanguagesAccessData] 36 | 37 | 38 | class TeamByProjectRole(TypedDict): 39 | name: TeamRole 40 | permissions: RolePermission 41 | 42 | 43 | class GroupTeamPatchRequest(TypedDict): 44 | op: PatchOperation 45 | path: str 46 | value: Any 47 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_memory/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_memory/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TranslationMemoryPatchPath(Enum): 5 | NAME = "/name" 6 | 7 | 8 | class TranslationMemorySegmentRecordOperation(Enum): 9 | ADD = "add" 10 | REPLACE = "replace" 11 | REMOVE = "remove" 12 | 13 | 14 | class TranslationMemorySegmentRecordOperationPath(Enum): 15 | ADD = "/records/-" 16 | REPLACE = "/records/{recordId}/text" 17 | REMOVE = "/records/{recordId}" 18 | 19 | 20 | class ListTmsOrderBy(Enum): 21 | ID = "id" 22 | NAME = "name" 23 | USER_ID = "userId" 24 | CREATED_AT = "createdAt" 25 | 26 | 27 | class ListTmSegmentsOrderBy(Enum): 28 | ID = "id" 29 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_memory/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.translation_memory.enums import ( 5 | TranslationMemoryPatchPath, 6 | TranslationMemorySegmentRecordOperation, 7 | TranslationMemorySegmentRecordOperationPath, 8 | ) 9 | from crowdin_api.typing import TypedDict 10 | 11 | 12 | class TranslationMemoryPatchRequest(TypedDict): 13 | value: Any 14 | op: PatchOperation 15 | path: TranslationMemoryPatchPath 16 | 17 | 18 | class TranslationMemorySegmentRecord(TypedDict): 19 | languageId: str 20 | text: str 21 | 22 | 23 | class TranslationMemorySegmentRecordOperationAdd(TypedDict): 24 | op: TranslationMemorySegmentRecordOperation 25 | path: TranslationMemorySegmentRecordOperationPath 26 | value: TranslationMemorySegmentRecord 27 | 28 | 29 | class TranslationMemorySegmentRecordOperationReplace(TypedDict): 30 | op: TranslationMemorySegmentRecordOperation 31 | path: TranslationMemorySegmentRecordOperationPath 32 | value: str 33 | 34 | 35 | class TranslationMemorySegmentRecordOperationRemove(TypedDict): 36 | op: TranslationMemorySegmentRecordOperation 37 | path: TranslationMemorySegmentRecordOperationPath 38 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_status/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_status/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Category(Enum): 5 | EMPTY = "empty" 6 | VARIABLES = "variables" 7 | TAGS = "tags" 8 | PUNCTUATION = "punctuation" 9 | SYMBOL_REGISTER = "symbol_register" 10 | SPACES = "spaces" 11 | SIZE = "size" 12 | SPECIAL_SYMBOLS = "special_symbols" 13 | WRONG_TRANSLATION = "wrong_translation" 14 | SPELLCHECK = "spellcheck" 15 | ICU = "icu" 16 | 17 | 18 | class Validation(Enum): 19 | EMPTY_STRING_CHECK = "empty_string_check" 20 | EMPTY_SUGGESTION_CHECK = "empty_suggestion_check" 21 | MAX_LENGTH_CHECK = "max_length_check" 22 | TAGS_CHECK = "tags_check" 23 | MISMATCH_IDS_CHECK = "mismatch_ids_check" 24 | CDATA_CHECK = "cdata_check" 25 | SPECIALS_SYMBOLS_CHECK = "specials_symbols_check" 26 | LEADING_NEWLINES_CHECK = "leading_newlines_check" 27 | TRAILING_NEWLINES_CHECK = "trailing_newlines_check" 28 | LEADING_SPACES_CHECK = "leading_spaces_check" 29 | TRAILING_SPACES_CHECK = "trailing_spaces_check" 30 | MULTIPLE_SPACES_CHECK = "multiple_spaces_check" 31 | CUSTOM_BLOCKED_VARIABLES_CHECK = "custom_blocked_variables_check" 32 | HIGHEST_PRIORITY_CUSTOM_VARIABLES_CHECK = "highest_priority_custom_variables_check" 33 | HIGHEST_PRIORITY_VARIABLES_CHECK = "highest_priority_variables_check" 34 | C_VARIABLES_CHECK = "c_variables_check" 35 | PYTHON_VARIABLES_CHECK = "python_variables_check" 36 | RAILS_VARIABLES_CHECK = "rails_variables_check" 37 | JAVA_VARIABLES_CHECK = "java_variables_check" 38 | DOT_NET_VARIABLES_CHECK = "dot_net_variables_check" 39 | TWIG_VARIABLES_CHECK = "twig_variables_check" 40 | PHP_VARIABLES_CHECK = "php_variables_check" 41 | FREEMARKER_VARIABLES_CHECK = "freemarker_variables_check" 42 | LOWEST_PRIORITY_VARIABLE_CHECK = "lowest_priority_variable_check" 43 | LOWEST_PRIORITY_CUSTOM_VARIABLES_CHECK = "lowest_priority_custom_variables_check" 44 | PUNCTUATION_CHECK = "punctuation_check" 45 | SPACES_BEFORE_PUNCTUATION_CHECK = "spaces_before_punctuation_check" 46 | SPACES_AFTER_PUNCTUATION_CHECK = "spaces_after_punctuation_check" 47 | NON_BREAKING_SPACES_CHECK = "non_breaking_spaces_check" 48 | CAPITALIZE_CHECK = "capitalize_check" 49 | MULTIPLE_UPPERCASE_CHECK = "multiple_uppercase_check" 50 | PARENTHESES_CHECK = "parentheses_check" 51 | ENTITIES_CHECK = "entities_check" 52 | ESCAPED_QUOTES_CHECK = "escaped_quotes_check" 53 | WRONG_TRANSLATION_ISSUE_CHECK = "wrong_translation_issue_check" 54 | SPELLCHECK = "spellcheck" 55 | ICU_CHECK = "icu_check" 56 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_status/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.translation_status.enums import Category, Validation 5 | 6 | 7 | class TranslationStatusResource(BaseResource): 8 | """ 9 | Resource for Translation Status. 10 | 11 | Status represents the general localization progress on both translations and proofreading. 12 | 13 | Use API to check translation and proofreading progress on different levels: 14 | file, language, branch, directory. 15 | 16 | Link to documentation: 17 | https://developer.crowdin.com/api/v2/#tag/Translation-Status 18 | """ 19 | 20 | def get_branch_progress( 21 | self, 22 | branchId: int, 23 | projectId: Optional[int] = None, 24 | page: Optional[int] = None, 25 | offset: Optional[int] = None, 26 | limit: Optional[int] = None, 27 | ): 28 | """ 29 | Get Branch Progress. 30 | 31 | Link to documentation: 32 | https://developer.crowdin.com/api/v2/#operation/api.projects.branches.languages.progress.getMany 33 | """ 34 | 35 | projectId = projectId or self.get_project_id() 36 | 37 | return self._get_entire_data( 38 | method="get", 39 | path=f"projects/{projectId}/branches/{branchId}/languages/progress", 40 | params=self.get_page_params(page=page, offset=offset, limit=limit), 41 | ) 42 | 43 | def get_directory_progress( 44 | self, 45 | directoryId: int, 46 | projectId: Optional[int] = None, 47 | page: Optional[int] = None, 48 | offset: Optional[int] = None, 49 | limit: Optional[int] = None, 50 | ): 51 | """ 52 | Get Directory Progress. 53 | 54 | Link to documentation: 55 | https://developer.crowdin.com/api/v2/#operation/api.projects.directories.languages.progress.getMany 56 | """ 57 | 58 | projectId = projectId or self.get_project_id() 59 | 60 | return self._get_entire_data( 61 | method="get", 62 | path=f"projects/{projectId}/directories/{directoryId}/languages/progress", 63 | params=self.get_page_params(page=page, offset=offset, limit=limit), 64 | ) 65 | 66 | def get_file_progress( 67 | self, 68 | fileId: int, 69 | projectId: Optional[int] = None, 70 | page: Optional[int] = None, 71 | offset: Optional[int] = None, 72 | limit: Optional[int] = None, 73 | ): 74 | """ 75 | Get File Progress. 76 | 77 | Link to documentation: 78 | https://developer.crowdin.com/api/v2/#operation/api.projects.files.languages.progress.getMany 79 | """ 80 | 81 | projectId = projectId or self.get_project_id() 82 | 83 | return self._get_entire_data( 84 | method="get", 85 | path=f"projects/{projectId}/files/{fileId}/languages/progress", 86 | params=self.get_page_params(page=page, offset=offset, limit=limit), 87 | ) 88 | 89 | def get_language_progress( 90 | self, 91 | languageId: str, 92 | projectId: Optional[int] = None, 93 | page: Optional[int] = None, 94 | offset: Optional[int] = None, 95 | limit: Optional[int] = None, 96 | ): 97 | """ 98 | Get Language Progress. 99 | 100 | Link to documentation: 101 | https://developer.crowdin.com/api/v2/#operation/api.projects.languages.files.progress.getMany 102 | """ 103 | 104 | projectId = projectId or self.get_project_id() 105 | 106 | return self._get_entire_data( 107 | method="get", 108 | path=f"projects/{projectId}/languages/{languageId}/progress", 109 | params=self.get_page_params(page=page, offset=offset, limit=limit), 110 | ) 111 | 112 | def get_project_progress( 113 | self, 114 | projectId: Optional[int] = None, 115 | languageIds: Optional[Iterable[str]] = None, 116 | page: Optional[int] = None, 117 | offset: Optional[int] = None, 118 | limit: Optional[int] = None, 119 | ): 120 | """ 121 | Get Project Progress. 122 | 123 | Link to documentation: 124 | https://developer.crowdin.com/api/v2/#operation/api.projects.languages.progress.getMany 125 | """ 126 | 127 | projectId = projectId or self.get_project_id() 128 | params = {"languageIds": None if languageIds is None else ",".join(languageIds)} 129 | params.update(self.get_page_params(page=page, offset=offset, limit=limit)) 130 | 131 | return self._get_entire_data( 132 | method="get", 133 | path=f"projects/{projectId}/languages/progress", 134 | params=params, 135 | ) 136 | 137 | def list_qa_check_issues( 138 | self, 139 | projectId: Optional[int] = None, 140 | category: Optional[Iterable[Category]] = None, 141 | validation: Optional[Iterable[Validation]] = None, 142 | languageIds: Optional[Iterable[str]] = None, 143 | page: Optional[int] = None, 144 | offset: Optional[int] = None, 145 | limit: Optional[int] = None, 146 | ): 147 | """ 148 | List QA Check Issues. 149 | 150 | Link to documentation: 151 | https://developer.crowdin.com/api/v2/#operation/api.projects.qa-checks.getMany 152 | """ 153 | 154 | projectId = projectId or self.get_project_id() 155 | params = { 156 | "languageIds": None if languageIds is None else ",".join(languageIds), 157 | "category": ",".join((item.value for item in category)) if category else None, 158 | "validation": ",".join((item.value for item in validation)) if validation else None, 159 | } 160 | params.update(self.get_page_params(page=page, offset=offset, limit=limit)) 161 | 162 | return self._get_entire_data( 163 | method="get", 164 | path=f"projects/{projectId}/qa-checks", 165 | params=params, 166 | ) 167 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translation_status/tests/test_translation_status_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from crowdin_api.api_resources.translation_status.enums import Category, Validation 5 | from crowdin_api.api_resources.translation_status.resource import TranslationStatusResource 6 | from crowdin_api.requester import APIRequester 7 | 8 | 9 | class TestTranslationStatusResource: 10 | resource_class = TranslationStatusResource 11 | 12 | def get_resource(self, base_absolut_url): 13 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 14 | 15 | def test_resource_with_id(self, base_absolut_url): 16 | project_id = 1 17 | resource = self.resource_class( 18 | requester=APIRequester(base_url=base_absolut_url), project_id=project_id 19 | ) 20 | assert resource.get_project_id() == project_id 21 | 22 | @mock.patch("crowdin_api.requester.APIRequester.request") 23 | def test_get_branch_progress(self, m_request, base_absolut_url): 24 | m_request.return_value = "response" 25 | 26 | resource = self.get_resource(base_absolut_url) 27 | assert resource.get_branch_progress(projectId=1, branchId=2, page=1) == "response" 28 | m_request.assert_called_once_with( 29 | method="get", 30 | params=resource.get_page_params(page=1, offset=None, limit=None), 31 | path="projects/1/branches/2/languages/progress", 32 | ) 33 | 34 | @mock.patch("crowdin_api.requester.APIRequester.request") 35 | def test_get_directory_progress(self, m_request, base_absolut_url): 36 | m_request.return_value = "response" 37 | 38 | resource = self.get_resource(base_absolut_url) 39 | assert resource.get_directory_progress(projectId=1, directoryId=2, page=1) == "response" 40 | m_request.assert_called_once_with( 41 | method="get", 42 | params=resource.get_page_params(page=1, offset=None, limit=None), 43 | path="projects/1/directories/2/languages/progress", 44 | ) 45 | 46 | @mock.patch("crowdin_api.requester.APIRequester.request") 47 | def test_get_file_progress(self, m_request, base_absolut_url): 48 | m_request.return_value = "response" 49 | 50 | resource = self.get_resource(base_absolut_url) 51 | assert resource.get_file_progress(projectId=1, fileId=2, page=1) == "response" 52 | m_request.assert_called_once_with( 53 | method="get", 54 | params=resource.get_page_params(page=1, offset=None, limit=None), 55 | path="projects/1/files/2/languages/progress", 56 | ) 57 | 58 | @mock.patch("crowdin_api.requester.APIRequester.request") 59 | def test_get_language_progress(self, m_request, base_absolut_url): 60 | m_request.return_value = "response" 61 | 62 | resource = self.get_resource(base_absolut_url) 63 | assert resource.get_language_progress(projectId=1, languageId="sr", page=1) == "response" 64 | m_request.assert_called_once_with( 65 | method="get", 66 | params=resource.get_page_params(page=1, offset=None, limit=None), 67 | path="projects/1/languages/sr/progress", 68 | ) 69 | 70 | @mock.patch("crowdin_api.requester.APIRequester.request") 71 | def test_get_project_progress(self, m_request, base_absolut_url): 72 | m_request.return_value = "response" 73 | resource = self.get_resource(base_absolut_url) 74 | 75 | params = resource.get_page_params(page=1, offset=None, limit=None) 76 | params["languageIds"] = "sr,rs" 77 | 78 | assert ( 79 | resource.get_project_progress(projectId=1, languageIds=["sr", "rs"], page=1) 80 | == "response" 81 | ) 82 | m_request.assert_called_once_with( 83 | method="get", 84 | params=params, 85 | path="projects/1/languages/progress", 86 | ) 87 | 88 | @pytest.mark.parametrize( 89 | "in_params,request_params", 90 | ( 91 | ( 92 | {}, 93 | { 94 | "languageIds": None, 95 | "category": None, 96 | "validation": None, 97 | "offset": 0, 98 | "limit": 25, 99 | }, 100 | ), 101 | ( 102 | { 103 | "languageIds": ["some", "string"], 104 | "category": [Category.ICU, Category.EMPTY], 105 | "validation": [Validation.ICU_CHECK, Validation.TAGS_CHECK], 106 | }, 107 | { 108 | "languageIds": "some,string", 109 | "category": "icu,empty", 110 | "validation": "icu_check,tags_check", 111 | "offset": 0, 112 | "limit": 25, 113 | }, 114 | ), 115 | ), 116 | ) 117 | @mock.patch("crowdin_api.requester.APIRequester.request") 118 | def test_list_qa_check_issues(self, m_request, in_params, request_params, base_absolut_url): 119 | m_request.return_value = "response" 120 | 121 | resource = self.get_resource(base_absolut_url) 122 | assert resource.list_qa_check_issues(projectId=1, **in_params) == "response" 123 | m_request.assert_called_once_with( 124 | method="get", path="projects/1/qa-checks", params=request_params 125 | ) 126 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translations/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translations/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PreTranslationApplyMethod(Enum): 5 | TM = "tm" 6 | MT = "mt" 7 | AI = "ai" 8 | 9 | 10 | class PreTranslationAutoApproveOption(Enum): 11 | ALL = "all" 12 | EXCEPT_AUTO_SUBSTITUTED = "exceptAutoSubstituted" 13 | PERFECT_MATCH_ONLY = "perfectMatchOnly" 14 | NONE = "none" 15 | 16 | 17 | class CharTransformation(Enum): 18 | ASIAN = "asian" 19 | EUROPEAN = "european" 20 | ARABIC = "arabic" 21 | CYRILLIC = "cyrillic" 22 | 23 | 24 | class PreTranslationEditOperation(Enum): 25 | REPLACE = "replace" 26 | TEST = "test" 27 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/translations/types.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | from crowdin_api.typing import TypedDict 3 | from crowdin_api.api_resources.translations.enums import PreTranslationEditOperation 4 | 5 | 6 | class FallbackLanguages(TypedDict): 7 | languageId: Iterable[str] 8 | 9 | 10 | class EditPreTranslationScheme(TypedDict): 11 | op: PreTranslationEditOperation 12 | path: str 13 | value: str 14 | 15 | 16 | class UploadTranslationRequest(TypedDict): 17 | storageId: int 18 | fileId: int 19 | importEqSuggestions: Optional[bool] 20 | autoApproveImported: Optional[bool] 21 | translateHidden: Optional[bool] 22 | addToTm: Optional[bool] 23 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/users/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/users/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class UserRole(Enum): 5 | ALL = "all" 6 | MANAGER = "manager" 7 | PROOFREADER = "proofreader" 8 | TRANSLATOR = "translator" 9 | BLOCKED = "blocked" 10 | 11 | 12 | class UserPatchPath(Enum): 13 | FIRST_NAME = "/firstName" 14 | LAST_NAME = "/lastName" 15 | TIMEZONE = "/timezone" 16 | STATUS = "/status" 17 | 18 | 19 | class ProjectRole(Enum): 20 | MANAGER = "manager" 21 | DEVELOPER = "developer" 22 | TRANSLATOR = "translator" 23 | PROOFREADER = "proofreader" 24 | LANGUAGE_COORDINATOR = "language_coordinator" 25 | MEMBER = "member" 26 | 27 | 28 | class ListProjectMembersCrowdinOrderBy(Enum): 29 | ID = "id" 30 | USERNAME = "username" 31 | FULL_NAME = "fullName" 32 | 33 | 34 | class ListUsersOrderBy(Enum): 35 | ID = "id" 36 | USERNAME = "username" 37 | FIRST_NAME = "firstName" 38 | LAST_NAME = "lastName" 39 | EMAIL = "email" 40 | STATUS = "status" 41 | CREATED_AT = "createdAt" 42 | LAST_SEEN = "lastSeen" 43 | 44 | 45 | class ListProjectMembersEnterpriseOrderBy(Enum): 46 | ID = "id" 47 | USERNAME = "username" 48 | FIRST_NAME = "firstName" 49 | LAST_NAME = "lastName" 50 | 51 | 52 | class ListGroupManagersOrderBy(Enum): 53 | ID = "id" 54 | USERNAME = "username" 55 | EMAIL = "email" 56 | STATUS = "status" 57 | CREATED_AT = "createdAt" 58 | LAST_SEEN = "lastSeen" 59 | 60 | 61 | class ListGroupTeamsOrderBy(Enum): 62 | ID = "id" 63 | NAME = "name" 64 | CREATED_AT = "createdAt" 65 | UPDATED_AT = "updatedAt" 66 | 67 | 68 | class OrganizationRole(Enum): 69 | ADMIN = "admin" 70 | MANAGER = "manager" 71 | VENDOR = "vendor" 72 | CLIENT = "client" 73 | 74 | 75 | class UserStatus(Enum): 76 | ACTIVE = "active" 77 | PENDING = "pending" 78 | BLOCKED = "blocked" 79 | 80 | 81 | class UserTwoFactorAuthStatus(Enum): 82 | ENABLED = "enabled" 83 | DISABLED = "disabled" 84 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/users/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Iterable 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.users.enums import UserPatchPath, ProjectRole 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class UserPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: UserPatchPath 12 | 13 | 14 | class LanguageData(TypedDict): 15 | allContent: bool 16 | workflowStepIds: Optional[Iterable[Any]] 17 | 18 | 19 | class LanguagesAccessData(TypedDict): 20 | it: LanguageData 21 | uk: LanguageData 22 | 23 | 24 | class RolePermission(TypedDict): 25 | allLanguages: bool 26 | languagesAccess: Optional[LanguagesAccessData] 27 | 28 | 29 | class ProjectMemberRole(TypedDict): 30 | name: ProjectRole 31 | permissions: RolePermission 32 | 33 | 34 | class GroupManagerPatchRequest(TypedDict): 35 | op: PatchOperation 36 | path: str 37 | value: Any 38 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/vendors/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/vendors/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | 5 | 6 | class VendorsResource(BaseResource): 7 | """ 8 | Resource for Vendors. 9 | 10 | Vendors are the organizations that provide professional translation services. 11 | To assign a Vendor to a project workflow you should invite an existing Organization 12 | to be a Vendor for you. 13 | 14 | Use API to get the list of the Vendors you already invited to your organization. 15 | 16 | Link to documentation: 17 | https://developer.crowdin.com/enterprise/api/v2/#tag/Vendors 18 | """ 19 | 20 | def list_vendors(self, offset: Optional[int] = None, limit: Optional[int] = None): 21 | """ 22 | List Teams. 23 | 24 | Link to documentation: 25 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.teams.getMany 26 | """ 27 | 28 | return self._get_entire_data( 29 | method="get", 30 | path="vendors", 31 | params=self.get_page_params(offset=offset, limit=limit), 32 | ) 33 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/vendors/tests/test_vendos_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from crowdin_api.api_resources.vendors.resource import VendorsResource 6 | from crowdin_api.requester import APIRequester 7 | 8 | 9 | class TestVendorsResources: 10 | resource_class = VendorsResource 11 | 12 | def get_resource(self, base_absolut_url): 13 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 14 | 15 | @pytest.mark.parametrize( 16 | "incoming_data, request_params", 17 | ( 18 | ( 19 | {}, 20 | { 21 | "limit": 25, 22 | "offset": 0, 23 | }, 24 | ), 25 | ( 26 | { 27 | "limit": 10, 28 | "offset": 2, 29 | }, 30 | { 31 | "limit": 10, 32 | "offset": 2, 33 | }, 34 | ), 35 | ), 36 | ) 37 | @mock.patch("crowdin_api.requester.APIRequester.request") 38 | def test_list_vendors(self, m_request, incoming_data, request_params, base_absolut_url): 39 | m_request.return_value = "response" 40 | 41 | resource = self.get_resource(base_absolut_url) 42 | assert resource.list_vendors(**incoming_data) == "response" 43 | m_request.assert_called_once_with( 44 | method="get", 45 | path="vendors", 46 | params=request_params, 47 | ) 48 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class WebhookEvents(Enum): 5 | FILE_TRANSLATED = "file.translated" 6 | FILE_APPROVED = "file.approved" 7 | PROJECT_TRANSLATED = "project.translated" 8 | PROJECT_APPROVED = "project.approved" 9 | TRANSLATION_UPDATED = "translation.updated" 10 | STRING_ADDED = "string.added" 11 | STRING_UPDATED = "string.updated" 12 | STRING_DELETED = "string.deleted" 13 | SUGGESTION_ADDED = "suggestion.added" 14 | SUGGESTION_UPDATED = "suggestion.updated" 15 | SUGGESTION_DELETED = "suggestion.deleted" 16 | SUGGESTION_APPROVED = "suggestion.approved" 17 | SUGGESTION_DISAPPROVED = "suggestion.disapproved" 18 | 19 | 20 | class WebhookRequestType(Enum): 21 | POST = "POST" 22 | GET = "GET" 23 | 24 | 25 | class WebhookContentType(Enum): 26 | MULTIPART_FORM_DATA = "multipart/form-data" 27 | APPLICATION_JSON = "application/json" 28 | APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded" 29 | 30 | 31 | class WebhookPatchPath(Enum): 32 | NAME = "/name" 33 | URL = "/url" 34 | IS_ACTIVE = "/isActive" 35 | BATCHING_ENABLED = "/batchingEnabled" 36 | CONTENT_TYPE = "/contentType" 37 | EVENTS = "/events" 38 | HEADERS = "/headers" 39 | REQUEST_TYPE = "/requestType" 40 | PAYLOAD = "/payload" 41 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/organization/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/organization/enums.py: -------------------------------------------------------------------------------- 1 | 2 | from enum import Enum 3 | 4 | 5 | class OrganizationWebhookEvent(Enum): 6 | PROJECT_CREATED = "project.created" 7 | PROJECT_DELETED = "project.deleted" 8 | 9 | 10 | class EnterpriseOrgWebhookEvent(Enum): 11 | GROUP_CREATED = "group.created" 12 | GROUP_DELETED = "group.deleted" 13 | PROJECT_CREATED = "project.created" 14 | PROJECT_DELETED = "project.deleted" 15 | 16 | 17 | class OrganizationWebhookPatchPath(Enum): 18 | NAME = "/name" 19 | URL = "/url" 20 | IS_ACTIVE = "/isActive" 21 | BATCHING_ENABLED = "/batchingEnabled" 22 | CONTENT_TYPE = "/contentType" 23 | EVENTS = "/events" 24 | HEADERS = "/headers" 25 | REQUEST_TYPE = "/requestType" 26 | PAYLOAD = "/payload" 27 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/organization/resource.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Optional, Iterable, Dict 3 | 4 | from crowdin_api.api_resources.abstract.resources import BaseResource 5 | from crowdin_api.api_resources.webhooks.enums import WebhookRequestType, WebhookContentType 6 | from crowdin_api.api_resources.webhooks.organization.enums import OrganizationWebhookEvent, EnterpriseOrgWebhookEvent 7 | from crowdin_api.api_resources.webhooks.organization.types import OrganizationWebhookPatchRequest 8 | 9 | 10 | class OrganizationWebhooksResource(BaseResource): 11 | BASE_URL = "/webhooks" 12 | 13 | def get_webhooks_path( 14 | self, 15 | organization_webhook_id: int 16 | ): 17 | return f"{self.BASE_URL}/{organization_webhook_id}" 18 | 19 | """Webhooks allow you to collect information about events that happen in your Crowdin account. You can select the 20 | request type, content type, and add a custom payload, which allows you to create integrations with other systems 21 | on your own. 22 | 23 | You can configure webhooks for the following events: 24 | 25 | - project is created 26 | - project is deleted 27 | 28 | Use API to create, modify, and delete specific webhooks. 29 | 30 | Link to documentation: 31 | https://developer.crowdin.com/api/v2/#tag/Organization-Webhooks 32 | """ 33 | 34 | def list_webhooks( 35 | self, 36 | page: Optional[int] = None, 37 | offset: Optional[int] = None, 38 | limit: Optional[int] = None 39 | ): 40 | """ 41 | List Webhooks 42 | 43 | Link to documentation: 44 | https://developer.crowdin.com/api/v2/#operation/api.webhooks.getMany 45 | """ 46 | return self._get_entire_data( 47 | method="get", 48 | path=self.BASE_URL, 49 | params=self.get_page_params( 50 | page=page, 51 | offset=offset, 52 | limit=limit 53 | ) 54 | ) 55 | 56 | def add_webhook( 57 | self, 58 | name: str, 59 | url: str, 60 | events: Iterable[OrganizationWebhookEvent], 61 | request_type: WebhookRequestType, 62 | is_active: Optional[bool] = None, 63 | batching_enabled: Optional[bool] = None, 64 | content_type: Optional[WebhookContentType] = None, 65 | headers: Optional[Dict] = None, 66 | payload: Optional[Dict] = None 67 | ): 68 | """ 69 | Add webhook 70 | For Enterprise please use method "add_webhook_enterprise" 71 | 72 | Link to documentation: 73 | https://developer.crowdin.com/api/v2/#operation/api.webhooks.post 74 | """ 75 | 76 | return self.requester.request( 77 | method="post", 78 | path=self.BASE_URL, 79 | request_data={ 80 | "name": name, 81 | "url": url, 82 | "events": events, 83 | "requestType": request_type, 84 | "isActive": is_active, 85 | "batchingEnabled": batching_enabled, 86 | "contentType": content_type, 87 | "headers": headers, 88 | "payload": payload 89 | } 90 | ) 91 | 92 | def add_webhook_enterprise( 93 | self, 94 | name: str, 95 | url: str, 96 | events: Iterable[EnterpriseOrgWebhookEvent], 97 | request_type: WebhookRequestType, 98 | is_active: Optional[bool] = None, 99 | batching_enabled: Optional[bool] = None, 100 | content_type: Optional[WebhookContentType] = None, 101 | headers: Optional[Dict] = None, 102 | payload: Optional[Dict] = None 103 | ): 104 | """ 105 | Add webhook (enterprise) 106 | Events list is different 107 | 108 | Link to documentation: 109 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.webhooks.post 110 | """ 111 | 112 | return self.requester.request( 113 | method="post", 114 | path=self.BASE_URL, 115 | request_data={ 116 | "name": name, 117 | "url": url, 118 | "events": events, 119 | "requestType": request_type, 120 | "isActive": is_active, 121 | "batchingEnabled": batching_enabled, 122 | "contentType": content_type, 123 | "headers": headers, 124 | "payload": payload 125 | } 126 | ) 127 | 128 | def get_webhook( 129 | self, 130 | organization_webhook_id: int 131 | ): 132 | """ 133 | Get webhook 134 | 135 | Link to documentation: 136 | https://developer.crowdin.com/api/v2/#operation/api.webhooks.get 137 | """ 138 | return self.requester.request( 139 | method="get", 140 | path=self.get_webhooks_path(organization_webhook_id) 141 | ) 142 | 143 | def delete_webhook( 144 | self, 145 | organization_webhook_id: int 146 | ): 147 | """ 148 | Delete webhook 149 | 150 | Link to documentation: 151 | https://developer.crowdin.com/api/v2/#operation/api.webhooks.delete 152 | """ 153 | 154 | return self.requester.request( 155 | method="delete", 156 | path=self.get_webhooks_path(organization_webhook_id) 157 | ) 158 | 159 | def edit_webhook( 160 | self, 161 | organization_webhook_id: int, 162 | data: Iterable[OrganizationWebhookPatchRequest] 163 | ): 164 | """ 165 | Edit webhook 166 | 167 | Link to documentation: 168 | https://developer.crowdin.com/api/v2/#operation/api.webhooks.patch 169 | """ 170 | 171 | return self.requester.request( 172 | method="patch", 173 | path=self.get_webhooks_path(organization_webhook_id), 174 | request_data=data 175 | ) 176 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/organization/types.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import TypedDict, Any 3 | 4 | from crowdin_api.api_resources.enums import PatchOperation 5 | from crowdin_api.api_resources.webhooks.organization.enums import OrganizationWebhookPatchPath 6 | 7 | 8 | class OrganizationWebhookPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: OrganizationWebhookPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Iterable, Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.api_resources.webhooks.enums import ( 5 | WebhookContentType, 6 | WebhookEvents, 7 | WebhookRequestType, 8 | ) 9 | from crowdin_api.api_resources.webhooks.types import WebhookPatchRequest 10 | 11 | 12 | class WebhooksResource(BaseResource): 13 | """ 14 | Resource for Webhooks. 15 | 16 | Webhooks allow you to collect information about events that happen in your Crowdin projects. 17 | You can select the request type, content type, and add a custom payload, which allows you to 18 | create integrations with other systems on your own. 19 | 20 | You can configure webhooks for the following events: 21 | -project file is fully translated 22 | -project file is fully reviewed 23 | -all strings in project are translated 24 | -all strings in project are reviewed 25 | -final translation of string is updated (using Replace in suggestions feature) 26 | -source string is added 27 | -source string is updated 28 | -source string is deleted 29 | -source string is translated 30 | -translation for source string is updated (using Replace in suggestions feature) 31 | -one of translations is deleted 32 | -translation for string is approved 33 | -approval for previously added translation is removed 34 | 35 | Use API to create, modify, and delete specific webhooks. 36 | 37 | Link to documentation: 38 | https://developer.crowdin.com/api/v2/#tag/Webhooks 39 | """ 40 | 41 | def get_webhooks_path(self, projectId: int, webhookId: Optional[int] = None): 42 | if webhookId: 43 | return f"projects/{projectId}/webhooks/{webhookId}" 44 | 45 | return f"projects/{projectId}/webhooks" 46 | 47 | def list_webhooks( 48 | self, 49 | projectId: Optional[int] = None, 50 | page: Optional[int] = None, 51 | offset: Optional[int] = None, 52 | limit: Optional[int] = None, 53 | ): 54 | """ 55 | List Webhooks. 56 | 57 | Link to documentation: 58 | https://developer.crowdin.com/api/v2/#tag/Webhooks 59 | """ 60 | 61 | projectId = projectId or self.get_project_id() 62 | 63 | return self._get_entire_data( 64 | method="get", 65 | path=self.get_webhooks_path(projectId=projectId), 66 | params=self.get_page_params(page=page, offset=offset, limit=limit), 67 | ) 68 | 69 | def add_webhook( 70 | self, 71 | name: str, 72 | url: str, 73 | events: Iterable[WebhookEvents], 74 | requestType: WebhookRequestType, 75 | projectId: Optional[int] = None, 76 | isActive: Optional[bool] = None, 77 | batchingEnabled: Optional[bool] = None, 78 | contentType: Optional[WebhookContentType] = None, 79 | headers: Optional[Dict] = None, 80 | payload: Optional[Dict] = None, 81 | ): 82 | """ 83 | Add Webhook. 84 | 85 | Link to documentation: 86 | https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.post 87 | """ 88 | 89 | projectId = projectId or self.get_project_id() 90 | 91 | return self.requester.request( 92 | method="post", 93 | path=self.get_webhooks_path(projectId=projectId), 94 | request_data={ 95 | "name": name, 96 | "url": url, 97 | "events": events, 98 | "requestType": requestType, 99 | "isActive": isActive, 100 | "batchingEnabled": batchingEnabled, 101 | "contentType": contentType, 102 | "headers": headers, 103 | "payload": payload, 104 | }, 105 | ) 106 | 107 | def get_webhook(self, webhookId: int, projectId: Optional[int] = None): 108 | """ 109 | Get Webhook. 110 | 111 | Link to documentation: 112 | https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.get 113 | """ 114 | 115 | projectId = projectId or self.get_project_id() 116 | 117 | return self.requester.request( 118 | method="get", 119 | path=self.get_webhooks_path(projectId=projectId, webhookId=webhookId), 120 | ) 121 | 122 | def delete_webhook(self, webhookId: int, projectId: Optional[int] = None): 123 | """ 124 | Delete Webhook. 125 | 126 | Link to documentation: 127 | https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.delete 128 | """ 129 | 130 | projectId = projectId or self.get_project_id() 131 | 132 | return self.requester.request( 133 | method="delete", 134 | path=self.get_webhooks_path(projectId=projectId, webhookId=webhookId), 135 | ) 136 | 137 | def edit_webhook( 138 | self, 139 | webhookId: int, 140 | data: Iterable[WebhookPatchRequest], 141 | projectId: Optional[int] = None, 142 | ): 143 | """ 144 | Edit Custom Language. 145 | 146 | Link to documentation: 147 | https://developer.crowdin.com/api/v2/#operation/api.projects.webhooks.patch 148 | """ 149 | 150 | projectId = projectId or self.get_project_id() 151 | 152 | return self.requester.request( 153 | method="patch", 154 | path=self.get_webhooks_path(projectId=projectId, webhookId=webhookId), 155 | request_data=data, 156 | ) 157 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/tests/test_webhooks_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from crowdin_api.api_resources.enums import PatchOperation 5 | from crowdin_api.api_resources.webhooks.enums import ( 6 | WebhookContentType, 7 | WebhookEvents, 8 | WebhookPatchPath, 9 | WebhookRequestType, 10 | ) 11 | from crowdin_api.api_resources.webhooks.resource import WebhooksResource 12 | from crowdin_api.requester import APIRequester 13 | 14 | 15 | class TestWebhooksResource: 16 | resource_class = WebhooksResource 17 | 18 | def get_resource(self, base_absolut_url): 19 | return self.resource_class(requester=APIRequester(base_url=base_absolut_url)) 20 | 21 | def test_resource_with_id(self, base_absolut_url): 22 | project_id = 1 23 | resource = self.resource_class( 24 | requester=APIRequester(base_url=base_absolut_url), project_id=project_id 25 | ) 26 | assert resource.get_project_id() == project_id 27 | 28 | @mock.patch("crowdin_api.requester.APIRequester.request") 29 | def test_list_webhooks(self, m_request, base_absolut_url): 30 | m_request.return_value = "response" 31 | 32 | resource = self.get_resource(base_absolut_url) 33 | assert resource.list_webhooks(projectId=1) == "response" 34 | m_request.assert_called_once_with( 35 | method="get", 36 | params=resource.get_page_params(), 37 | path=resource.get_webhooks_path(projectId=1), 38 | ) 39 | 40 | @pytest.mark.parametrize( 41 | "in_params, request_data", 42 | ( 43 | ( 44 | { 45 | "name": "Some", 46 | "url": "https://example.com", 47 | "events": [WebhookEvents.SUGGESTION_ADDED], 48 | "requestType": WebhookRequestType.POST, 49 | }, 50 | { 51 | "name": "Some", 52 | "url": "https://example.com", 53 | "events": [WebhookEvents.SUGGESTION_ADDED], 54 | "requestType": WebhookRequestType.POST, 55 | "isActive": None, 56 | "batchingEnabled": None, 57 | "contentType": None, 58 | "headers": None, 59 | "payload": None, 60 | }, 61 | ), 62 | ( 63 | { 64 | "name": "Some", 65 | "url": "https://example.com", 66 | "events": [WebhookEvents.SUGGESTION_ADDED], 67 | "requestType": WebhookRequestType.POST, 68 | "isActive": True, 69 | "batchingEnabled": False, 70 | "contentType": WebhookContentType.MULTIPART_FORM_DATA, 71 | "headers": {}, 72 | "payload": {}, 73 | }, 74 | { 75 | "name": "Some", 76 | "url": "https://example.com", 77 | "events": [WebhookEvents.SUGGESTION_ADDED], 78 | "requestType": WebhookRequestType.POST, 79 | "isActive": True, 80 | "batchingEnabled": False, 81 | "contentType": WebhookContentType.MULTIPART_FORM_DATA, 82 | "headers": {}, 83 | "payload": {}, 84 | }, 85 | ), 86 | ), 87 | ) 88 | @mock.patch("crowdin_api.requester.APIRequester.request") 89 | def test_add_webhook(self, m_request, in_params, request_data, base_absolut_url): 90 | m_request.return_value = "response" 91 | 92 | resource = self.get_resource(base_absolut_url) 93 | assert resource.add_webhook(projectId=1, **in_params) == "response" 94 | m_request.assert_called_once_with( 95 | method="post", 96 | path=resource.get_webhooks_path(projectId=1), 97 | request_data=request_data, 98 | ) 99 | 100 | @mock.patch("crowdin_api.requester.APIRequester.request") 101 | def test_get_webhook(self, m_request, base_absolut_url): 102 | m_request.return_value = "response" 103 | 104 | resource = self.get_resource(base_absolut_url) 105 | assert resource.get_webhook(projectId=1, webhookId=2) == "response" 106 | m_request.assert_called_once_with( 107 | method="get", path=resource.get_webhooks_path(projectId=1, webhookId=2) 108 | ) 109 | 110 | @mock.patch("crowdin_api.requester.APIRequester.request") 111 | def test_delete_webhookk(self, m_request, base_absolut_url): 112 | m_request.return_value = "response" 113 | 114 | resource = self.get_resource(base_absolut_url) 115 | assert resource.delete_webhook(projectId=1, webhookId=2) == "response" 116 | m_request.assert_called_once_with( 117 | method="delete", path=resource.get_webhooks_path(projectId=1, webhookId=2) 118 | ) 119 | 120 | @mock.patch("crowdin_api.requester.APIRequester.request") 121 | def test_edit_webhook(self, m_request, base_absolut_url): 122 | m_request.return_value = "response" 123 | 124 | data = [ 125 | { 126 | "value": "test", 127 | "op": PatchOperation.REPLACE, 128 | "path": WebhookPatchPath.NAME, 129 | } 130 | ] 131 | 132 | resource = self.get_resource(base_absolut_url) 133 | assert resource.edit_webhook(projectId=1, webhookId=2, data=data) == "response" 134 | m_request.assert_called_once_with( 135 | method="patch", 136 | request_data=data, 137 | path=resource.get_webhooks_path(projectId=1, webhookId=2), 138 | ) 139 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/webhooks/types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from crowdin_api.api_resources.enums import PatchOperation 4 | from crowdin_api.api_resources.webhooks.enums import WebhookPatchPath 5 | from crowdin_api.typing import TypedDict 6 | 7 | 8 | class WebhookPatchRequest(TypedDict): 9 | value: Any 10 | op: PatchOperation 11 | path: WebhookPatchPath 12 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/workflows/__init__.py: -------------------------------------------------------------------------------- 1 | __pdoc__ = {'tests': False} 2 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/workflows/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ListWorkflowStepStringsOrderBy(Enum): 5 | ID = "id" 6 | TEXT = "text" 7 | IDENTIFIER = "identifier" 8 | CONTEXT = "context" 9 | CREATED_AT = "createdAt" 10 | UPDATED_AT = "updatedAt" 11 | -------------------------------------------------------------------------------- /crowdin_api/api_resources/workflows/resource.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from crowdin_api.api_resources.abstract.resources import BaseResource 4 | from crowdin_api.sorting import Sorting 5 | 6 | 7 | class WorkflowsResource(BaseResource): 8 | """ 9 | Resource for Workflows. 10 | 11 | Workflows are the sequences of steps that content in your project should go through 12 | (e.g. pre-translation, translation, proofreading). You can use a default template or create 13 | the one that works best for you and assign it to the needed projects. 14 | 15 | Use API to get the list of workflow templates available in your organization and to check 16 | the details of a specific template. 17 | 18 | Link to documentation: 19 | https://developer.crowdin.com/enterprise/api/v2/#tag/Workflows 20 | """ 21 | 22 | def get_workflow_steps_path(self, projectId: int, stepId: Optional[int] = None): 23 | if stepId: 24 | return f"projects/{projectId}/workflow-steps/{stepId}" 25 | 26 | return f"projects/{projectId}/workflow-steps" 27 | 28 | def get_workflow_templates_path(self, templateId: Optional[int] = None): 29 | if templateId: 30 | return f"workflow-templates/{templateId}" 31 | 32 | return "workflow-templates" 33 | 34 | def list_workflow_steps(self, projectId: Optional[int] = None): 35 | """ 36 | List Workflow Steps. 37 | 38 | Link to documentation: 39 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.getMany 40 | """ 41 | projectId = projectId or self.get_project_id() 42 | 43 | return self._get_entire_data( 44 | method="get", 45 | path=self.get_workflow_steps_path(projectId=projectId), 46 | ) 47 | 48 | def get_workflow_step(self, stepId: int, projectId: Optional[int] = None): 49 | """ 50 | Get Workflow Step. 51 | 52 | Link to documentation: 53 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.get 54 | """ 55 | projectId = projectId or self.get_project_id() 56 | 57 | return self.requester.request( 58 | method="get", 59 | path=self.get_workflow_steps_path(projectId=projectId, stepId=stepId), 60 | ) 61 | 62 | def list_workflow_templates( 63 | self, 64 | groupId: Optional[int] = None, 65 | limit: Optional[int] = None, 66 | offset: Optional[int] = None 67 | ): 68 | """ 69 | List Workflow Templates. 70 | 71 | Link to documentation: 72 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.workflow-templates.getMany 73 | """ 74 | params = {"groupId": groupId} 75 | params.update(self.get_page_params(offset=offset, limit=limit)) 76 | 77 | return self._get_entire_data( 78 | method="get", 79 | path=self.get_workflow_templates_path(), 80 | params=params, 81 | ) 82 | 83 | def get_workflow_template(self, templateId: int): 84 | """ 85 | Get Workflow Template. 86 | 87 | Link to documentation: 88 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.workflow-templates.get 89 | """ 90 | return self.requester.request( 91 | method="get", 92 | path=self.get_workflow_templates_path(templateId=templateId), 93 | ) 94 | 95 | def get_workflow_step_strings_path(self, projectId: int, stepId: int): 96 | return f"projects/{projectId}/workflow-steps/{stepId}/strings" 97 | 98 | def list_workflow_step_strings( 99 | self, 100 | projectId: Optional[int], 101 | stepId: int, 102 | languageIds: Optional[str] = None, 103 | orderBy: Optional[Sorting] = None, 104 | status: Optional[str] = None, 105 | limit: Optional[int] = None, 106 | offset: Optional[int] = None 107 | ): 108 | """ 109 | List Strings on the Workflow Step. 110 | 111 | Link to documentation: 112 | https://developer.crowdin.com/enterprise/api/v2/#operation/api.projects.workflow-steps.strings.getMany 113 | """ 114 | projectId = projectId or self.get_project_id() 115 | 116 | params = { 117 | "languageIds": languageIds, 118 | "orderBy": orderBy, 119 | "status": status 120 | } 121 | params.update(self.get_page_params(offset=offset, limit=limit)) 122 | 123 | return self._get_entire_data( 124 | method="get", 125 | path=self.get_workflow_step_strings_path(projectId=projectId, stepId=stepId), 126 | params=params 127 | ) 128 | -------------------------------------------------------------------------------- /crowdin_api/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class PlatformType(Enum): 5 | BASIC = auto() 6 | ENTERPRISE = auto() 7 | -------------------------------------------------------------------------------- /crowdin_api/exceptions.py: -------------------------------------------------------------------------------- 1 | from crowdin_api import status 2 | 3 | 4 | class CrowdinException(Exception): 5 | def __init__(self, detail=None): 6 | super().__init__(detail) 7 | self._detail = detail 8 | 9 | @property 10 | def message(self): 11 | return self._detail 12 | 13 | def __str__(self): 14 | return "{class_name}: {message}".format( 15 | class_name=self.__class__.__name__, message=self.message 16 | ) 17 | 18 | def __repr__(self): 19 | return self.__str__() 20 | 21 | 22 | class APIException(CrowdinException): 23 | default_http_status = None 24 | template = ( 25 | "http_status={exc.http_status}, " 26 | "request_id={exc.request_id}, " 27 | "detail: {exc._detail}, " 28 | "context={exc.context}" 29 | ) 30 | 31 | def __init__( 32 | self, 33 | detail=None, 34 | context=None, 35 | http_status=None, 36 | headers=None, 37 | should_retry=None, 38 | ): 39 | super().__init__(detail=detail) 40 | self.context = context 41 | self.headers = headers or {} 42 | self.http_status = http_status or self.default_http_status 43 | 44 | if should_retry is None: 45 | if ( 46 | http_status is None 47 | or 100 <= http_status <= 199 48 | or 300 <= http_status <= 499 49 | ): 50 | should_retry = False 51 | else: 52 | should_retry = True 53 | 54 | self.should_retry = should_retry 55 | 56 | @property 57 | def request_id(self): 58 | return self.headers.get("request-id", None) 59 | 60 | @property 61 | def message(self): 62 | return self.template.format(exc=self) 63 | 64 | 65 | class ParsingError(APIException): 66 | detail = "Error while parsing." 67 | 68 | 69 | class AuthenticationFailed(APIException): 70 | default_http_status = status.HTTP_401_UNAUTHORIZED 71 | detail = "Incorrect authentication credentials" 72 | 73 | 74 | class PermissionDenied(APIException): 75 | default_http_status = status.HTTP_403_FORBIDDEN 76 | detail = "You do not have permission to perform this action" 77 | 78 | 79 | class NotFound(APIException): 80 | default_http_status = status.HTTP_404_NOT_FOUND 81 | detail = "Not found." 82 | 83 | 84 | class MethodNotAllowed(APIException): 85 | default_http_status = status.HTTP_405_METHOD_NOT_ALLOWED 86 | detail = "Method not allowed." 87 | 88 | 89 | class Throttled(APIException): 90 | default_http_status = status.HTTP_429_TOO_MANY_REQUESTS 91 | detail = "Request was throttled." 92 | 93 | 94 | class ValidationError(APIException): 95 | default_http_status = status.HTTP_400_BAD_REQUEST 96 | detail = "Invalid input." 97 | -------------------------------------------------------------------------------- /crowdin_api/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture() 5 | def base_absolut_url(): 6 | return "https://api.crowdin.com/api/v2/" 7 | 8 | 9 | @pytest.fixture() 10 | def base_url(): 11 | return "api.crowdin.com/api/v2/" 12 | -------------------------------------------------------------------------------- /crowdin_api/parser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import re 4 | from enum import Enum 5 | 6 | from crowdin_api.sorting import Sorting 7 | 8 | 9 | class CrowdinJSONEncoder(json.JSONEncoder): 10 | def default(self, obj): 11 | if isinstance(obj, datetime.datetime): 12 | offset = obj.utcoffset() 13 | 14 | if offset < datetime.timedelta(): 15 | sign = "-" 16 | offset = offset.seconds / 60 - 24 * 60 17 | else: 18 | sign = "+" 19 | offset = offset.seconds / 60 20 | 21 | offset = abs(int(offset)) 22 | 23 | return "{date_time}{sign}{offset_h:02}:{offset_m:02}".format( 24 | date_time=obj.strftime("%Y-%m-%dT%H:%M:%S"), 25 | sign=sign, 26 | offset_h=offset // 60, 27 | offset_m=offset % 60, 28 | ) 29 | 30 | if isinstance(obj, Enum): 31 | try: 32 | return super().default(obj.value) 33 | except TypeError: 34 | return obj.value 35 | 36 | if isinstance(obj, Sorting): 37 | return str(obj) 38 | 39 | return super().default(obj) 40 | 41 | 42 | class CrowdinJSONDecoder(json.JSONDecoder): 43 | datetime_re = re.compile( 44 | r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" 45 | r"[T ](?P\d{1,2}):(?P\d{1,2})" 46 | r"(?::(?P\d{1,2})(?:[\.,](?P\d{1,6})\d{0,6})?)?" 47 | r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" 48 | ) 49 | 50 | def _get_timezone(self, tzinfo): 51 | if tzinfo == "Z": 52 | return datetime.timezone.utc 53 | 54 | elif tzinfo is not None: 55 | offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0 56 | offset = 60 * int(tzinfo[1:3]) + offset_mins 57 | return datetime.timezone( 58 | datetime.timedelta(minutes=-offset if tzinfo[0] == "-" else offset), 59 | "{sign}{h:02}{m:02}".format( 60 | sign=tzinfo[0], h=offset // 60, m=offset % 60 61 | ), 62 | ) 63 | 64 | def _parse_datetime(self, value): 65 | match = self.datetime_re.match(value) 66 | if match: 67 | kw = match.groupdict() 68 | kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0") 69 | tzinfo = self._get_timezone(kw.pop("tzinfo")) 70 | kw = {k: int(v) for k, v in kw.items() if v is not None} 71 | kw["tzinfo"] = tzinfo 72 | return datetime.datetime(**kw) 73 | 74 | def __init__(self, *args, **kwargs): 75 | super().__init__(*args, object_hook=self.object_hook, **kwargs) 76 | 77 | def object_hook(self, parsed_object): 78 | for key, value in parsed_object.items(): 79 | if not isinstance(value, (str, bytes)): 80 | continue 81 | 82 | date_value = self._parse_datetime(value) 83 | 84 | if isinstance(date_value, datetime.datetime): 85 | parsed_object[key] = date_value 86 | 87 | return parsed_object 88 | 89 | 90 | def dumps(obj): 91 | return json.dumps(obj, cls=CrowdinJSONEncoder) 92 | 93 | 94 | def loads(s): 95 | return json.loads(s, cls=CrowdinJSONDecoder) 96 | -------------------------------------------------------------------------------- /crowdin_api/sorting.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional, List 3 | 4 | 5 | class SortingOrder(Enum): 6 | ASC = "asc" 7 | DESC = "desc" 8 | 9 | def __str__(self): 10 | return self.value 11 | 12 | def __eq__(self, other): 13 | if isinstance(other, SortingOrder): 14 | return self.value == other.value 15 | return False 16 | 17 | 18 | class SortingRule: 19 | def __init__(self, rule: Enum, order: Optional[SortingOrder] = None): 20 | if not isinstance(rule, Enum): 21 | raise ValueError("Rule must be of type Enum.") 22 | if not rule.value: 23 | raise ValueError("Rule value cannot be empty.") 24 | if isinstance(rule, SortingOrder): 25 | raise ValueError("Rule cannot be of type SortingOrder.") 26 | if order and not isinstance(order, SortingOrder): 27 | raise ValueError("Order must be of type SortingOrder.") 28 | 29 | self.rule = rule.value 30 | self.order = order 31 | 32 | def __str__(self): 33 | return f"{self.rule} {self.order}" if self.order else self.rule 34 | 35 | def __eq__(self, other): 36 | if not isinstance(other, SortingRule): 37 | return False 38 | return self.rule == other.rule and self.order == other.order 39 | 40 | 41 | class Sorting: 42 | def __init__(self, rules: List[SortingRule]): 43 | self.rules = rules 44 | 45 | def __str__(self): 46 | return ",".join([str(rule) for rule in self.rules]) 47 | 48 | def __eq__(self, other): 49 | if not isinstance(other, Sorting): 50 | return False 51 | return self.rules == other.rules 52 | -------------------------------------------------------------------------------- /crowdin_api/status.py: -------------------------------------------------------------------------------- 1 | HTTP_100_CONTINUE = 100 2 | HTTP_101_SWITCHING_PROTOCOLS = 101 3 | HTTP_200_OK = 200 4 | HTTP_201_CREATED = 201 5 | HTTP_202_ACCEPTED = 202 6 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 7 | HTTP_204_NO_CONTENT = 204 8 | HTTP_205_RESET_CONTENT = 205 9 | HTTP_206_PARTIAL_CONTENT = 206 10 | HTTP_207_MULTI_STATUS = 207 11 | HTTP_208_ALREADY_REPORTED = 208 12 | HTTP_226_IM_USED = 226 13 | HTTP_300_MULTIPLE_CHOICES = 300 14 | HTTP_301_MOVED_PERMANENTLY = 301 15 | HTTP_302_FOUND = 302 16 | HTTP_303_SEE_OTHER = 303 17 | HTTP_304_NOT_MODIFIED = 304 18 | HTTP_305_USE_PROXY = 305 19 | HTTP_306_RESERVED = 306 20 | HTTP_307_TEMPORARY_REDIRECT = 307 21 | HTTP_308_PERMANENT_REDIRECT = 308 22 | HTTP_400_BAD_REQUEST = 400 23 | HTTP_401_UNAUTHORIZED = 401 24 | HTTP_402_PAYMENT_REQUIRED = 402 25 | HTTP_403_FORBIDDEN = 403 26 | HTTP_404_NOT_FOUND = 404 27 | HTTP_405_METHOD_NOT_ALLOWED = 405 28 | HTTP_406_NOT_ACCEPTABLE = 406 29 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 30 | HTTP_408_REQUEST_TIMEOUT = 408 31 | HTTP_409_CONFLICT = 409 32 | HTTP_410_GONE = 410 33 | HTTP_411_LENGTH_REQUIRED = 411 34 | HTTP_412_PRECONDITION_FAILED = 412 35 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 36 | HTTP_414_REQUEST_URI_TOO_LONG = 414 37 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 38 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 39 | HTTP_417_EXPECTATION_FAILED = 417 40 | HTTP_418_IM_A_TEAPOT = 418 41 | HTTP_422_UNPROCESSABLE_ENTITY = 422 42 | HTTP_423_LOCKED = 423 43 | HTTP_424_FAILED_DEPENDENCY = 424 44 | HTTP_426_UPGRADE_REQUIRED = 426 45 | HTTP_428_PRECONDITION_REQUIRED = 428 46 | HTTP_429_TOO_MANY_REQUESTS = 429 47 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 48 | HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 49 | HTTP_500_INTERNAL_SERVER_ERROR = 500 50 | HTTP_501_NOT_IMPLEMENTED = 501 51 | HTTP_502_BAD_GATEWAY = 502 52 | HTTP_503_SERVICE_UNAVAILABLE = 503 53 | HTTP_504_GATEWAY_TIMEOUT = 504 54 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 55 | HTTP_506_VARIANT_ALSO_NEGOTIATES = 506 56 | HTTP_507_INSUFFICIENT_STORAGE = 507 57 | HTTP_508_LOOP_DETECTED = 508 58 | HTTP_509_BANDWIDTH_LIMIT_EXCEEDED = 509 59 | HTTP_510_NOT_EXTENDED = 510 60 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 61 | -------------------------------------------------------------------------------- /crowdin_api/tests/test_client_methods.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from crowdin_api import CrowdinClient 4 | from crowdin_api.exceptions import CrowdinException 5 | 6 | 7 | def test_groups_without_organization(): 8 | client = CrowdinClient() 9 | 10 | with pytest.raises(CrowdinException, match="Not implemented for the base API"): 11 | client.groups.list_groups() 12 | 13 | 14 | def test_vendors_without_organization(): 15 | client = CrowdinClient() 16 | 17 | with pytest.raises(CrowdinException, match="Not implemented for the base API"): 18 | client.vendors.list_vendors() 19 | 20 | 21 | def test_teams_without_organization(): 22 | client = CrowdinClient() 23 | 24 | with pytest.raises(CrowdinException, match="Not implemented for the base API"): 25 | client.teams.list_teams() 26 | 27 | 28 | def test_workflows_without_organization(): 29 | client = CrowdinClient() 30 | 31 | with pytest.raises(CrowdinException, match="Not implemented for the base API"): 32 | client.workflows.list_workflow_templates() 33 | -------------------------------------------------------------------------------- /crowdin_api/tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from unittest.mock import Mock, PropertyMock 3 | 4 | import pytest 5 | from crowdin_api import status 6 | from crowdin_api.exceptions import APIException, CrowdinException 7 | 8 | 9 | class TestCrowdinException: 10 | def test_init(self): 11 | detail = "some detail" 12 | exception = CrowdinException(detail=detail) 13 | assert exception._detail == detail 14 | 15 | def test_message(self): 16 | detail = "some detail" 17 | exception = CrowdinException(detail=detail) 18 | assert exception.message == detail 19 | 20 | def test_str(self): 21 | detail = "some detail" 22 | exception = CrowdinException(detail=detail) 23 | assert str(exception) == "CrowdinException: some detail" 24 | assert exception.__repr__() == "CrowdinException: some detail" 25 | 26 | 27 | class TestAPIException: 28 | @pytest.mark.parametrize( 29 | "status_code,should_retry,result", 30 | ( 31 | (status.HTTP_500_INTERNAL_SERVER_ERROR, None, True), 32 | (status.HTTP_500_INTERNAL_SERVER_ERROR, False, False), 33 | (status.HTTP_400_BAD_REQUEST, None, False), 34 | (status.HTTP_400_BAD_REQUEST, True, True), 35 | (status.HTTP_100_CONTINUE, None, False), 36 | (status.HTTP_100_CONTINUE, True, True), 37 | (status.HTTP_301_MOVED_PERMANENTLY, None, False), 38 | (status.HTTP_301_MOVED_PERMANENTLY, True, True), 39 | ), 40 | ) 41 | def test_init(self, status_code, should_retry, result): 42 | context = "some detail" 43 | exception = APIException( 44 | context=context, http_status=status_code, should_retry=should_retry 45 | ) 46 | assert exception.should_retry == result 47 | 48 | @pytest.mark.parametrize( 49 | "headers,result", 50 | ( 51 | ({"request-id": 1}, 1), 52 | ({"request-ids": 1}, None), 53 | ), 54 | ) 55 | def test_request_id(self, headers, result): 56 | exception = APIException(headers=headers) 57 | assert exception.request_id == result 58 | 59 | @mock.patch("crowdin_api.exceptions.APIException.template", new_callable=PropertyMock) 60 | def test_message(self, m_template): 61 | m_template.return_value = "template" 62 | 63 | exc = APIException(http_status=1, headers={"request-id": 2}, context=3) 64 | 65 | assert exc.message == "template" 66 | 67 | template = Mock() 68 | m_template.return_value = template 69 | exc = APIException() 70 | _ = exc.message 71 | 72 | template.format.assert_called_once_with(exc=exc) 73 | -------------------------------------------------------------------------------- /crowdin_api/tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from enum import Enum 3 | 4 | import pytest 5 | from crowdin_api.parser import dumps, loads 6 | from crowdin_api.sorting import Sorting, SortingOrder, SortingRule 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "in_value, out_value", 11 | ( 12 | ('{"int": 1}', {"int": 1}), 13 | ('{"float": 3.14}', {"float": 3.14}), 14 | ('{"string": "some string"}', {"string": "some string"}), 15 | ( 16 | '{"datetime": "1988-01-04T18:20:00+02:00"}', 17 | { 18 | "datetime": datetime.datetime( 19 | year=1988, 20 | month=1, 21 | day=4, 22 | hour=18, 23 | minute=20, 24 | second=0, 25 | tzinfo=datetime.timezone(datetime.timedelta(minutes=60 * 2), "+0200"), 26 | ) 27 | }, 28 | ), 29 | ( 30 | '{"datetime": "1988-01-04T18:20:00+02:30"}', 31 | { 32 | "datetime": datetime.datetime( 33 | year=1988, 34 | month=1, 35 | day=4, 36 | hour=18, 37 | minute=20, 38 | second=0, 39 | tzinfo=datetime.timezone(datetime.timedelta(minutes=60 * 2 + 30), "+0230"), 40 | ) 41 | }, 42 | ), 43 | ( 44 | '{"datetime": "1988-01-04T18:20:00-02:00"}', 45 | { 46 | "datetime": datetime.datetime( 47 | year=1988, 48 | month=1, 49 | day=4, 50 | hour=18, 51 | minute=20, 52 | second=0, 53 | tzinfo=datetime.timezone(datetime.timedelta(minutes=-60 * 2), "-0200"), 54 | ) 55 | }, 56 | ), 57 | ( 58 | '{"datetime": "1988-01-04T18:20:00-02:30"}', 59 | { 60 | "datetime": datetime.datetime( 61 | year=1988, 62 | month=1, 63 | day=4, 64 | hour=18, 65 | minute=20, 66 | second=0, 67 | tzinfo=datetime.timezone(datetime.timedelta(minutes=-60 * 2 - 30), "-0230"), 68 | ) 69 | }, 70 | ), 71 | ), 72 | ) 73 | def test_parser(in_value, out_value): 74 | load_result = loads(in_value) 75 | assert load_result == out_value 76 | 77 | dumps_result = dumps(load_result) 78 | assert dumps_result == in_value 79 | 80 | 81 | class TestEnum(Enum): 82 | ID = "id" 83 | CREATED_AT = "createdAt" 84 | 85 | 86 | def test_sorting_serialization(): 87 | sorting = Sorting( 88 | [ 89 | SortingRule(TestEnum.ID, SortingOrder.DESC), 90 | SortingRule(TestEnum.CREATED_AT, SortingOrder.ASC), 91 | ] 92 | ) 93 | 94 | result = dumps({"orderBy": sorting}) 95 | expected = '{"orderBy": "id desc,createdAt asc"}' 96 | 97 | assert result == expected 98 | 99 | 100 | def test_enum_serialization(): 101 | enum_value = TestEnum.ID 102 | result = dumps(enum_value) 103 | expected = '"id"' 104 | assert result == expected 105 | 106 | 107 | def test_parser_default_fallback(): 108 | class UnserializableObject: 109 | pass 110 | 111 | with pytest.raises(TypeError): 112 | dumps({"test": UnserializableObject()}) 113 | -------------------------------------------------------------------------------- /crowdin_api/tests/test_sorting.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from crowdin_api.sorting import SortingOrder, SortingRule, Sorting 3 | from enum import Enum 4 | 5 | 6 | class TestOrderBy(Enum): 7 | TEST = "test" 8 | OTHER = "other" 9 | LAST = "last" 10 | 11 | 12 | class TestSorting: 13 | def test_sorting_order_str(self): 14 | assert str(SortingOrder.ASC) == "asc" 15 | assert str(SortingOrder.DESC) == "desc" 16 | 17 | def test_sorting_order_equality(self): 18 | assert SortingOrder.ASC == SortingOrder.ASC 19 | assert SortingOrder.ASC != SortingOrder.DESC 20 | assert SortingOrder.ASC != "asc" 21 | 22 | def test_sorting_rule_creation(self): 23 | rule = SortingRule(TestOrderBy.TEST) 24 | assert str(rule) == "test" 25 | 26 | rule_with_order = SortingRule(TestOrderBy.TEST, SortingOrder.ASC) 27 | assert str(rule_with_order) == "test asc" 28 | 29 | def test_sorting_rule_validation(self): 30 | with pytest.raises(ValueError, match="Rule must be of type Enum"): 31 | SortingRule("not_enum") 32 | 33 | with pytest.raises(ValueError, match="Rule cannot be of type SortingOrder"): 34 | SortingRule(SortingOrder.ASC) 35 | 36 | with pytest.raises(ValueError, match="Order must be of type SortingOrder"): 37 | SortingRule(TestOrderBy.TEST, "asc") 38 | 39 | class EmptyEnum(Enum): 40 | EMPTY = "" 41 | 42 | with pytest.raises(ValueError, match="Rule value cannot be empty"): 43 | SortingRule(EmptyEnum.EMPTY) 44 | 45 | def test_sorting_rule_equality(self): 46 | rule1 = SortingRule(TestOrderBy.TEST, SortingOrder.ASC) 47 | rule2 = SortingRule(TestOrderBy.TEST, SortingOrder.ASC) 48 | rule3 = SortingRule(TestOrderBy.OTHER, SortingOrder.ASC) 49 | 50 | assert rule1 == rule2 51 | assert rule1 != rule3 52 | assert rule1 != "test asc" 53 | 54 | def test_sorting_multiple_rules(self): 55 | rules = [ 56 | SortingRule(TestOrderBy.TEST, SortingOrder.ASC), 57 | SortingRule(TestOrderBy.OTHER, SortingOrder.DESC), 58 | ] 59 | sorting = Sorting(rules) 60 | assert str(sorting) == "test asc,other desc" 61 | 62 | def test_sorting_multiple_rules_two(self): 63 | rules = [ 64 | SortingRule(TestOrderBy.TEST, SortingOrder.ASC), 65 | SortingRule(TestOrderBy.OTHER), 66 | SortingRule(TestOrderBy.LAST, SortingOrder.DESC), 67 | ] 68 | sorting = Sorting(rules) 69 | assert str(sorting) == "test asc,other,last desc" 70 | 71 | def test_sorting_equality(self): 72 | rules1 = [SortingRule(TestOrderBy.TEST)] 73 | rules2 = [SortingRule(TestOrderBy.TEST)] 74 | rules3 = [SortingRule(TestOrderBy.OTHER)] 75 | 76 | sort1 = Sorting(rules1) 77 | sort2 = Sorting(rules2) 78 | sort3 = Sorting(rules3) 79 | 80 | assert sort1 == sort2 81 | assert sort1 != sort3 82 | assert sort1 != "test" 83 | -------------------------------------------------------------------------------- /crowdin_api/typing.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 8): 4 | from typing import TypedDict # noqa F401 5 | else: 6 | from typing_extensions import TypedDict # noqa F401 7 | -------------------------------------------------------------------------------- /crowdin_api/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Optional, Iterable, Callable 3 | 4 | 5 | def convert_to_query_string( 6 | collection: Optional[Iterable], 7 | converter: Optional[Callable[[object], str]] = None 8 | ) -> Optional[str]: 9 | if not collection: 10 | return None 11 | 12 | if converter is not None: 13 | return ','.join(converter(item) for item in collection) 14 | else: 15 | return ','.join(str(item) for item in collection) 16 | 17 | 18 | def convert_enum_to_string_if_exists(value: Optional[Enum]) -> Optional[str]: 19 | return value.value if value is not None else None 20 | 21 | 22 | def convert_enum_collection_to_string_if_exists(value: Optional[Iterable[Enum]]) -> Optional[str]: 23 | if value is None: 24 | return None 25 | return ','.join([item.value for item in value if isinstance(item, Enum)]) 26 | -------------------------------------------------------------------------------- /requirements/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | black==23.3.0 4 | pre-commit==3.4.0 5 | xenon==0.9.1 6 | doc8==1.1.1 7 | pytest==7.4.1 8 | pytest-cov==4.1.0 9 | requests-mock==1.11.0 10 | flake8==6.1.0 -------------------------------------------------------------------------------- /requirements/requirements-doc.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | pdoc3==0.10.0 4 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | wheel 2 | setuptools 3 | 4 | requests>=2.31.0 5 | deprecated -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # All configuration for plugins and other utils is defined here. 2 | # Read more about `setup.cfg`: 3 | # https://docs.python.org/3/distutils/configfile.html 4 | 5 | [flake8] 6 | max-complexity = 6 7 | statistics = True 8 | max-line-length = 120 9 | doctests = True 10 | 11 | # Flake plugins: 12 | inline-quotes = double 13 | isort-show-traceback = True 14 | no-accept-encodings = True 15 | 16 | # Disable some checks: 17 | ignore = 18 | # missing trailing comma 19 | C812, 20 | # missing trailing comma in Python 3.5+ 21 | C815, 22 | # missing trailing comma in Python 3.6+ 23 | C816, 24 | # function is too complex 25 | C901, 26 | # Missing docstring in public module 27 | D100, 28 | # Missing docstring in public package 29 | D104, 30 | # No blank lines allowed after function docstring 31 | D202, 32 | # First line should be in imperative mood 33 | D401, 34 | # Whitespace before ':' 35 | E203, 36 | # Block quote ends without a blank line; unexpected unindent. 37 | RST201, 38 | # Definition list ends without a blank line; unexpected unindent. 39 | RST203, 40 | # Unexpected indentation. 41 | RST301, 42 | # Line break occurred before a binary operator 43 | W503, 44 | 45 | max-methods = 9 46 | max-module-members = 9 47 | 48 | [isort] 49 | # See https://github.com/timothycrosley/isort#multi-line-output-modes 50 | multi_line_output = 3 51 | include_trailing_comma = true 52 | sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 53 | default_section = FIRSTPARTY 54 | # Should be: 100 - 1 55 | line_length = 99 56 | wrap_length = 99 57 | 58 | [doc8] 59 | ignore-path = docs/_build 60 | max-line-length = 100 61 | sphinx = True 62 | 63 | 64 | [tool:pytest] 65 | python_files = tests.py test_*.py *_tests.py 66 | 67 | # Directories that are not visited by pytest collector: 68 | norecursedirs = *.egg .eggs dist build docs .tox .git __pycache__ 69 | 70 | # You will need to measure your tests speed with `-n auto` and without it, 71 | # so you can see whether it gives you any performance gain, or just gives 72 | # you an overhead. See `docs/template/development-process.rst`. 73 | addopts = 74 | --cache-clear 75 | --strict-markers 76 | --tb=short 77 | --doctest-modules 78 | --cov=crowdin_api 79 | --cov-report=term-missing:skip-covered 80 | --cov-report=xml 81 | --cov-fail-under=95 82 | 83 | [coverage:run] 84 | branch = True 85 | omit = *tests*,*fixtures.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | import os 3 | import re 4 | import sys 5 | from codecs import open 6 | 7 | from setuptools import find_packages, setup 8 | from setuptools.command.test import test 9 | 10 | ROOT = os.path.dirname(__file__) 11 | VERSION_RE = re.compile(r"""__version__ = ['"]([0-9.]+)['"]""") 12 | 13 | 14 | class PyTest(test): 15 | def run_tests(self): 16 | import pytest 17 | 18 | errno = pytest.main( 19 | [ 20 | "--strict-markers", 21 | "--tb=short", 22 | "--doctest-modules", 23 | "--cov=crowdin_api", 24 | "--cov-report=term-missing:skip-covered", 25 | "--cov-report=html", 26 | "--cov-fail-under=95", 27 | ] 28 | ) 29 | sys.exit(errno) 30 | 31 | 32 | with open("README.md", encoding="utf-8") as f: 33 | README = f.read() 34 | 35 | 36 | def get_version(): 37 | init = open(os.path.join(ROOT, "crowdin_api", "__init__.py")).read() 38 | return VERSION_RE.search(init).group(1) 39 | 40 | 41 | setup( 42 | name="crowdin-api-client", 43 | version=get_version(), 44 | description="Python client library for Crowdin API v2", 45 | long_description=README, 46 | long_description_content_type="text/markdown", 47 | author="Сrowdin", 48 | author_email="support@crowdin.com", 49 | url="https://github.com/crowdin/crowdin-api-client-python", 50 | packages=find_packages(exclude=["*tests*", "*fixtures.py"]), 51 | package_dir={"crowdin_api": "crowdin_api"}, 52 | python_requires=">=3.8", 53 | license="MIT", 54 | install_requires=[ 55 | "requests>=2.31.0", 56 | "typing-extensions; python_version < '3.8.0'", 57 | "deprecated" 58 | ], 59 | classifiers=[ 60 | "Programming Language :: Python", 61 | "Programming Language :: Python :: 3", 62 | "Programming Language :: Python :: 3.8", 63 | "Programming Language :: Python :: 3.9", 64 | "Programming Language :: Python :: 3.10", 65 | "Programming Language :: Python :: 3.11", 66 | "Programming Language :: Python :: 3.12", 67 | ], 68 | project_urls={ 69 | "Documentation": "https://support.crowdin.com/api/v2/", 70 | "Source Code": "https://github.com/crowdin/crowdin-api-client-python", 71 | }, 72 | cmdclass={"test": PyTest}, 73 | tests_require=[ 74 | "doc8==1.1.1", 75 | "pytest==7.4.1", 76 | "pytest-cov==4.1.0", 77 | "requests-mock==1.11.0", 78 | "types-six", 79 | "types-requests", 80 | ], 81 | ) 82 | --------------------------------------------------------------------------------