diff --git a/.changes/0.0.10.md b/.changes/0.0.10.md index dc19126d..95c22450 100644 --- a/.changes/0.0.10.md +++ b/.changes/0.0.10.md @@ -1,10 +1,6 @@ ## 0.0.10 - 2022-02-17 ### Added -* [#155](https://github.com/edgarrmondragon/citric/pull/155) Experimental support for Python 3.11 -* [#165](https://github.com/edgarrmondragon/citric/pull/165) Test in Windows and MacOS - - * [#192](https://github.com/edgarrmondragon/citric/pull/192) Implement `copy_survey` in client * [#190](https://github.com/edgarrmondragon/citric/pull/190) Implement `import_group` in client * [#196](https://github.com/edgarrmondragon/citric/pull/196) Implement `list_groups` in client diff --git a/.changes/1.0.0.md b/.changes/1.0.0.md new file mode 100644 index 00000000..d6b6cfde --- /dev/null +++ b/.changes/1.0.0.md @@ -0,0 +1,9 @@ +## 1.0.0 - 2024-02-12 +### Added +* [#1079](https://github.com/edgarrmondragon/citric/issues/1079) Added `get_db_version` method to RPC client +* [#1092](https://github.com/edgarrmondragon/citric/issues/1092) Added a `Session.call` method to allow calling RPC methods without any error handling to get the raw response +### Removed +* [#1093](https://github.com/edgarrmondragon/citric/issues/1093) Removed the `Session._headers` attribute +### Documentation +* [#1057](https://github.com/edgarrmondragon/citric/issues/1057) Documented `conda install` option +* [#1092](https://github.com/edgarrmondragon/citric/issues/1092) Linked unimplemented methods to `Session.call` examples diff --git a/.changes/1.0.1.md b/.changes/1.0.1.md new file mode 100644 index 00000000..b60df14d --- /dev/null +++ b/.changes/1.0.1.md @@ -0,0 +1,5 @@ +## 1.0.1 - 2024-06-12 +### Fixed +* [#1101](https://github.com/edgarrmondragon/citric/issues/1101) fix: Bump min `requests` to `2.25.1` (released 2020-12-16) +### Documentation +* [#1152](https://github.com/edgarrmondragon/citric/issues/1152) Fixed links to LimeSurvey API docs \ No newline at end of file diff --git a/.changes/unreleased/Added-20240117-233210.yaml b/.changes/unreleased/Added-20240117-233210.yaml deleted file mode 100644 index bcdc682e..00000000 --- a/.changes/unreleased/Added-20240117-233210.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: Add `get_db_version` method to RPC client -time: 2024-01-17T23:32:10.890143-06:00 -custom: - Issue: "1079" diff --git a/.changes/unreleased/Documentation-20231217-085221.yaml b/.changes/unreleased/Documentation-20231217-085221.yaml deleted file mode 100644 index 82b65c32..00000000 --- a/.changes/unreleased/Documentation-20231217-085221.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Documentation -body: 'docs: Document `conda install` option' -time: 2023-12-17T08:52:21.702919-06:00 -custom: - Issue: "1057" diff --git a/.circleci/config.yml b/.circleci/config.yml index 149e5f4e..6f733e21 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ version: 2.1 orbs: # The python orb contains a set of prepackaged CircleCI configuration you can use repeatedly in your configuration files # Orb commands and jobs help you with common scripting around a language/tool - # so you dont have to copy and paste it everywhere. + # so you don't have to copy and paste it everywhere. # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python python: circleci/python@1.5.0 @@ -21,7 +21,8 @@ jobs: # The executor is the environment in which the steps below will be executed - below will use a python 3.10.2 container # Change the version below to your required version of python docker: - - image: cimg/python:3.11.3 + - image: cimg/python:3.11.7 + - image: cimg/python:3.12.1 # Checkout the code as the first step. This is a dedicated CircleCI step. # The python orb's install-packages step will install the dependencies from a Pipfile via Pipenv by default. # Here we're making sure we use just use the system-wide pip. By default it uses the project root's requirements.txt. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..1cf72b68 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Global owner +* @edgarrmondragon diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..e963678a --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1 @@ +See the contributing guide in the [docs](https://citric.readthedocs.io/en/latest/contributing/code-of-conduct.html) for more information. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index d71e1bec..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -github: [edgarrmondragon] -patreon: edgarrmondragon -ko_fi: edgarrmondragon diff --git a/.github/ISSUE_TEMPLATE/BUG.yml b/.github/ISSUE_TEMPLATE/BUG.yml index c3ae0149..8e1967e1 100644 --- a/.github/ISSUE_TEMPLATE/BUG.yml +++ b/.github/ISSUE_TEMPLATE/BUG.yml @@ -15,7 +15,7 @@ body: attributes: label: Citric Version description: Version of the package you are using - placeholder: "0.10.0" + placeholder: "1.0.1" validations: required: true - type: dropdown diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index 86034b4a..77bab159 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -3,7 +3,7 @@ description: Install tools for Python projects inputs: constraints: - default: ".github/workflows/constraints.txt" + default: "${{ github.workspace }}/.github/workflows/constraints.txt" description: "Path to pip constraints file" required: true os: @@ -31,18 +31,18 @@ runs: with open(os.environ["GITHUB_ENV"], mode="a") as io: print(f"VIRTUALENV_PIP={pip.__version__}", file=io) - - name: Install Hatch + - name: Install Nox shell: bash env: PIP_CONSTRAINT: ${{ inputs.constraints }} run: | - pipx install hatch --verbose - hatch --version + pipx install nox + nox --version - - name: Install Nox + - name: Install uv shell: bash env: PIP_CONSTRAINT: ${{ inputs.constraints }} run: | - pipx install nox - nox --version + pipx install uv + uv --version diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b6ca4979..7483a50d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,24 +21,6 @@ updates: update-types: - "patch" - "minor" - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: monthly - reviewers: - - "edgarrmondragon" - commit-message: - prefix: "ci: " - groups: - artifacts: - patterns: - - "actions/*-load-artifact" - ci-dependencies: - update-types: - - "patch" - - "minor" - exclude-patterns: - - "actions/*-load-artifact" - package-ecosystem: docker directory: "/" schedule: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..52ec75c3 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +## Summary + + + +## Test Plan + + + +## Checklist + + + +- [ ] My pull request has a descriptive title. +- [ ] I have read the [CONTRIBUTING] guide. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] If appropriate, I have added necessary documentation. +- [ ] For user-facing changes, refactorings, performance improvements or documentation updates, I have added a changelog entry using [Changie]. + +For both the title of the PR and the changelog entry, prefer simple past tense or constructions with "now". For example: + + - Added `Client.invite_participants()` + - `Client.user_activation_settings()` now accepts a `user_activation_settings` keyword argument + +[CONTRIBUTING]: https://citric.readthedocs.io/en/latest/contributing/code-of-conduct.html +[Changie]: https://changie.dev/ diff --git a/.github/workflows/api-changes.yml b/.github/workflows/api-changes.yml index c1676f09..8a361c07 100644 --- a/.github/workflows/api-changes.yml +++ b/.github/workflows/api-changes.yml @@ -22,22 +22,21 @@ jobs: NOXSESSION: api steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: 3.12 - name: Install tools env: - PIP_CONSTRAINT: .github/workflows/constraints.txt + PIP_CONSTRAINT: ${{ github.workspace }}/.github/workflows/constraints.txt run: | python -Im pip install -U pip - pipx install griffe - pipx install nox + pipx install griffe nox uv pipx list - name: Set REF diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cf83748..40b81a25 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,11 +20,31 @@ jobs: name: Build wheel and sdist runs-on: ubuntu-latest steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 ref: ${{ github.event.inputs.tag || github.ref }} - - uses: hynek/build-and-inspect-python-package@c9fea028dc9c880c4d00d54727eff3fb1190d082 # v2.0.0 + - uses: hynek/build-and-inspect-python-package@2dbbf2b252d3a3c7cec7a810e3ed5983bd17b13a # v2.8.0 + + upload-to-release: + name: Upload to GitHub Release + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + needs: [build] + permissions: + contents: write # IMPORTANT: mandatory for making GitHub Releases + + steps: + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: Packages + path: dist + - uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # 2.9.0 + with: + file: dist/** + tag: ${{ github.event.inputs.tag || github.ref }} + overwrite: false + file_glob: true publish: name: Publish to PyPI @@ -38,11 +58,11 @@ jobs: id-token: write # IMPORTANT: mandatory for trusted publishing steps: - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: Packages path: dist - - uses: pypa/gh-action-pypi-publish@2f6f737ca5f74c637829c0f5c3acd0e29ea5e8bf # v1.8.11 + - uses: pypa/gh-action-pypi-publish@8a08d616893759ef8e1aa1f2785787c0b97e20d6 # v1.10.0 # Move this up when PyPI supports signing sign: @@ -51,22 +71,22 @@ jobs: runs-on: ubuntu-latest needs: [build] permissions: - contents: write # IMPORTANT: mandatory for making GitHub Releases - id-token: write # IMPORTANT: mandatory for sigstore + contents: write # IMPORTANT: mandatory for making GitHub Releases + id-token: write # IMPORTANT: mandatory for attestations + attestations: write # IMPORTANT: mandatory for attestations steps: - - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: Packages path: dist - - uses: sigstore/gh-action-sigstore-python@61f6a500bbfdd9a2a339cf033e5421951fbc1cd2 # v2.1.1 + - uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 + id: attest with: - inputs: >- - ./dist/*.tar.gz - ./dist/*.whl - - uses: svenstaro/upload-release-action@1beeb572c19a9242f4361f4cee78f8e0d9aec5df # v2 + subject-path: "./dist/citric*" + - uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # 2.9.0 with: - file: dist/** + file: ${{ steps.attest.outputs.bundle-path }} tag: ${{ github.event.inputs.tag || github.ref }} overwrite: false - file_glob: true + asset_name: attestations.intoto.jsonl diff --git a/.github/workflows/constraints.txt b/.github/workflows/constraints.txt index c0288fe6..c7f76f0e 100644 --- a/.github/workflows/constraints.txt +++ b/.github/workflows/constraints.txt @@ -1,5 +1,5 @@ -griffe==0.38.1 -hatch==1.9.1 -nox==2023.4.22 -pip==23.3.2 -pip-tools==7.3.0 +griffe==1.2.0 +nox==2024.4.15 +pip==24.2 +pip-tools==7.4.1 +uv==0.4.1 diff --git a/.github/workflows/gen-release-pr.yml b/.github/workflows/gen-release-pr.yml index 2d0d64cb..47a3fa40 100644 --- a/.github/workflows/gen-release-pr.yml +++ b/.github/workflows/gen-release-pr.yml @@ -23,8 +23,12 @@ permissions: read-all jobs: generate-pr: runs-on: ubuntu-latest + permissions: + contents: write # to create a github release + pull-requests: write # to create and update PRs + discussions: write # to create a discussion steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Batch changes uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 with: @@ -56,7 +60,7 @@ jobs: - name: Draft Release id: draft-release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 + uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 with: draft: true body_path: ".changes/${{ steps.latest.outputs.output }}.md" @@ -72,7 +76,7 @@ jobs: private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ steps.generate-token.outputs.token }} title: "chore: Release ${{ steps.latest.outputs.output }}" diff --git a/.github/workflows/gha-update.yml b/.github/workflows/gha-update.yml new file mode 100644 index 00000000..98627896 --- /dev/null +++ b/.github/workflows/gha-update.yml @@ -0,0 +1,43 @@ +name: Update GitHub Actions + +on: + workflow_dispatch: + schedule: + # Monthly + - cron: '0 0 1 * *' + +permissions: read-all + +jobs: + generate-pr: + runs-on: ubuntu-latest + permissions: + pull-requests: write # to create and update PRs + steps: + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 + with: + python-version: '3.x' + - uses: hynek/setup-cached-uv@4b4bfa932036976749a9653b0fa4fa10b1a7092b # v2.1.0 + - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} + - run: | + uvx gha-update + - uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 + with: + token: ${{ steps.generate-token.outputs.token }} + title: "chore: Update GitHub Actions" + branch: chore/update-gha + commit-message: "chore: Update GitHub Actions" + body: | + Update GitHub Actions to the latest versions. + + Uses https://github.com/davidism/gha-update. + reviewers: | + edgarrmondragon + assignees: | + edgarrmondragon + delete-branch: true + labels: Release diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 75fe8dc0..b4f148aa 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -33,12 +33,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif @@ -60,7 +60,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: SARIF file path: results.sarif @@ -68,6 +68,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@012739e5082ff0c22ca6d6ab32e07c36df03c4a4 # v3.22.12 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: results.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e64f420a..efa5c726 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,7 @@ on: - pyproject.toml - .github/workflows/tests.yml - .github/workflows/constraints.txt + - .github/actions/install-tools/action.yml push: branches: - 'main' @@ -22,6 +23,7 @@ on: - pyproject.toml - .github/workflows/tests.yml - .github/workflows/constraints.txt + - .github/actions/install-tools/action.yml schedule: - cron: "25 7 */3 * *" workflow_dispatch: @@ -43,7 +45,7 @@ env: jobs: tests: - name: "Test ${{ matrix.python-version }}${{ matrix.nightly && ' (nightly) ' || ' ' }}${{ matrix.nogil && ' (nogil) ' || ' ' }}/ ${{ matrix.os }}" + name: "Test ${{ matrix.python-version }} ${{ matrix.nightly && '(nightly) ' || '' }}/ ${{ matrix.os }}" runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental || false }} env: @@ -58,33 +60,16 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy3.10" os: ["ubuntu-latest"] include: - - python-version: "3.13" - os: "ubuntu-latest" - session: "tests" - experimental: true - - - python-version: "3.13" + - python-version: "3.14" os: "ubuntu-latest" session: "tests" experimental: true nightly: true - # - python-version: "3.13" - # os: "ubuntu-latest" - # session: "tests" - # experimental: true - # nightly: true - # nogil: true - - # - python-version: "3.14" - # os: "ubuntu-latest" - # session: "tests" - # experimental: true - # nightly: true - - python-version: "3.12" os: "windows-latest" session: "tests" @@ -95,13 +80,13 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-tags: true - name: Setup Python ${{ matrix.python-version }} if: "${{ !matrix.nightly }}" - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: ${{ matrix.python-version }} architecture: x64 @@ -116,7 +101,6 @@ jobs: uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 with: python-version: "${{ matrix.python-version }}-dev" - # nogil: ${{ matrix.nogil }} - name: Install tools uses: ./.github/actions/install-tools @@ -126,8 +110,9 @@ jobs: nox --verbose -s tests - name: Upload coverage data - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: + include-hidden-files: true name: "coverage-unit-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.nightly && 'nightly' || 'stable' }}" path: ".coverage.*" @@ -150,18 +135,19 @@ jobs: tags: ${{ steps.tags.outputs.tags }} steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: - python-version: 3.12 + python-version: 3.x architecture: x64 allow-prereleases: true cache: pip cache-dependency-path: | pyproject.toml - .github/workflows/constraint.txt + requirements/*.txt + .github/workflows/constraints.txt - name: Install tools uses: ./.github/actions/install-tools @@ -188,7 +174,7 @@ jobs: engines: ${{ steps.engines.outputs.engines }} steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Test against all engines if: ${{ contains(github.event.pull_request.labels.*.name, 'Release') || inputs.all_integrations }} @@ -252,11 +238,6 @@ jobs: database: postgres # Test Limesurvey/LimeSurvey branches - - python-version: "3.12" - ref: refs/heads/5.x - context: https://github.com/martialblog/docker-limesurvey.git#master:5.0/apache - database: postgres - - python-version: "3.12" ref: refs/heads/develop context: https://github.com/martialblog/docker-limesurvey.git#master:6.0/apache @@ -269,12 +250,12 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-tags: true - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: ${{ matrix.python-version }} architecture: x64 @@ -294,7 +275,7 @@ jobs: echo "LS_CHECKSUM=$(shasum -a 256 ls.tar.gz | cut -d' ' -f1)" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - name: Get Docker uses: actions-hub/docker/cli@f5fdbfc3f9d2a9265ead8962c1314108a7b7ec5d # v1.0.3 @@ -326,8 +307,9 @@ jobs: - name: Upload coverage data if: always() - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: + include-hidden-files: true name: "coverage-integration-${{ matrix.python-version }}-${{ matrix.image_tag || env.LS_CHECKSUM }}-${{ matrix.database }}" path: ".coverage.*" @@ -343,19 +325,19 @@ jobs: fail-fast: false steps: - name: Check out the repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Python - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: - python-version: "3.12" + python-version: "3.x" cache: pip - name: Install tools uses: ./.github/actions/install-tools - name: Download coverage data - uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: pattern: "coverage-${{ matrix.flag }}-*" merge-multiple: true @@ -370,7 +352,7 @@ jobs: nox -- xml - name: Upload coverage report - uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd257a34..3b6c03d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-json @@ -21,38 +21,38 @@ repos: - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.27.3 + rev: 0.29.2 hooks: - id: check-dependabot - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.14 + rev: v0.6.3 hooks: - id: ruff - name: Ruff lint args: [--fix, --exit-non-zero-on-fix, --show-fixes] - - id: ruff - name: Ruff format - entry: ruff format + - id: ruff-format - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell + # TODO: Use inline ignores, e.g. # codespell:ignore intoto + # https://github.com/codespell-project/codespell/issues/3387 + args: [-L, intoto] additional_dependencies: - tomli - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: - - pydoclint==0.3.8 + - pydoclint==0.4.1 - repo: https://github.com/pre-commit/pre-commit - rev: v3.6.0 + rev: v3.8.0 hooks: - id: validate_manifest @@ -63,13 +63,17 @@ repos: args: [--all] - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.7.0" + rev: "2.2.1" hooks: - id: pyproject-fmt -- repo: https://github.com/jazzband/pip-tools - rev: 7.3.0 +- repo: https://github.com/astral-sh/uv-pre-commit + rev: 0.4.2 hooks: - id: pip-compile files: ^pyproject\.toml$ - args: ["--extra", "docs", "-o", "docs/requirements.txt"] + args: ["pyproject.toml", "--universal", "--pre", "--python-version", "3.12", "--extra", "docs", "-o", "docs/requirements.txt"] + language_version: python3.12 + - id: pip-compile + files: ^pyproject\.toml$ + args: ["pyproject.toml", "--universal", "--resolution", "lowest-direct", "-o", "requirements/requirements-lowest-direct.txt"] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 859c2cf6..07bb6bc4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,9 +1,9 @@ version: 2 build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.11" + python: "3.12" jobs: post_checkout: - git fetch --unshallow || true diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b97c1f4..3e4c08d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and is generated by [Changie](https://github.com/miniscruff/changie). +## 1.0.1 - 2024-06-12 +### Fixed +* [#1101](https://github.com/edgarrmondragon/citric/issues/1101) fix: Bump min `requests` to `2.25.1` (released 2020-12-16) +### Documentation +* [#1152](https://github.com/edgarrmondragon/citric/issues/1152) Fixed links to LimeSurvey API docs + +## 1.0.0 - 2024-02-12 +### Added +* [#1079](https://github.com/edgarrmondragon/citric/issues/1079) Added `get_db_version` method to RPC client +* [#1092](https://github.com/edgarrmondragon/citric/issues/1092) Added a `Session.call` method to allow calling RPC methods without any error handling to get the raw response +### Removed +* [#1093](https://github.com/edgarrmondragon/citric/issues/1093) Removed the `Session._headers` attribute +### Documentation +* [#1057](https://github.com/edgarrmondragon/citric/issues/1057) Documented `conda install` option +* [#1092](https://github.com/edgarrmondragon/citric/issues/1092) Linked unimplemented methods to `Session.call` examples + + ## 0.10.0 - 2023-11-29 ### Added * [#994](https://github.com/edgarrmondragon/citric/issues/994) Experimental support for the new REST API @@ -143,10 +160,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie). ## 0.0.10 - 2022-02-17 ### Added -* [#155](https://github.com/edgarrmondragon/citric/pull/155) Experimental support for Python 3.11 -* [#165](https://github.com/edgarrmondragon/citric/pull/165) Test in Windows and MacOS - - * [#192](https://github.com/edgarrmondragon/citric/pull/192) Implement `copy_survey` in client * [#190](https://github.com/edgarrmondragon/citric/pull/190) Implement `import_group` in client * [#196](https://github.com/edgarrmondragon/citric/pull/196) Implement `list_groups` in client diff --git a/CITATION.cff b/CITATION.cff index b72266ac..7c58077e 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,7 +5,7 @@ authors: given-names: "Edgar" orcid: "https://orcid.org/0000-0002-4182-0385" title: "Citric" -version: "0.10.0" +version: "1.0.1" doi: 10.5281/zenodo.10216279 date-released: 2021-11-11 url: "https://github.com/edgarrmondragon/citric" diff --git a/README.md b/README.md index fa90e5c1..a200c451 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,11 @@ - + \n", " \n", - "
TestsProject Health + + + pre-commit.ci status @@ -79,17 +82,6 @@ - Tested against LimeSurvey 6.0.0+ and 5.0.0+ versions. - Experimental support for the new [REST API](https://manual.limesurvey.org/REST_API). -### Integration tests - -Integration tests are run against a LimeSurvey instance, and both PostgreSQL and MySQL backends, using Docker Compose. The following versions of LimeSurvey were tested for this release: - -- [6.4.1](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.4.1+240108) -- [6.4.0](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.4.0+231218) -- [6.3.9](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.3.9+231211) -- [5.6.50](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.50+240109) -- [5.6.49](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.49+231212) -- [5.6.48](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.48+231205) - ## Installation ```sh @@ -134,9 +126,11 @@ If you'd like to contribute to this project, please see the [contributing guide] ## Credits +- The [LimeSurvey][limesurvey-site] team for providing a great survey platform. - [Markus Opolka][martialblog] for maintaining a very robust set of [LimeSurvey Docker images](https://github.com/martialblog/docker-limesurvey/). - [Claudio Jolowicz][claudio] and [his amazing blog post][hypermodern]. [claudio]: https://twitter.com/cjolowicz/ [hypermodern]: https://cjolowicz.github.io/posts/hypermodern-python-01-setup/ +[limesurvey-site]: https://www.limesurvey.org/ [martialblog]: https://github.com/martialblog/ diff --git a/code_samples/duckdb_sql.py b/code_samples/duckdb_sql.py index 7ab1d7af..092bd733 100644 --- a/code_samples/duckdb_sql.py +++ b/code_samples/duckdb_sql.py @@ -2,9 +2,11 @@ from __future__ import annotations -# ruff: noqa: I001, PTH123 +# ruff: noqa: I001, PTH123, FURB103 # start example +from pathlib import Path + import citric import duckdb @@ -14,8 +16,7 @@ "secret", ) -with open("responses.csv", "wb") as file: - file.write(client.export_responses(12345, file_format="csv")) +Path("responses.csv").write_bytes(client.export_responses(12345, file_format="csv")) duckdb.execute("CREATE TABLE responses AS SELECT * FROM 'responses.csv'") duckdb.sql(""" diff --git a/code_samples/ruff.toml b/code_samples/ruff.toml index 8eaab689..a6c5fca3 100644 --- a/code_samples/ruff.toml +++ b/code_samples/ruff.toml @@ -1,4 +1,6 @@ extend = "../pyproject.toml" + +[lint] ignore = [ "INP001", # implicit-namespace-package ] diff --git a/code_samples/session_attr.py b/code_samples/session_attr.py index 1cb19d59..21de4284 100644 --- a/code_samples/session_attr.py +++ b/code_samples/session_attr.py @@ -11,6 +11,9 @@ "secret", ) -# Call the not_available_in_client method, not available in the RPC class -new_survey_id = client.session.not_available_in_client(35239, "copied_survey") +# Get the raw response from mail_registered_participants +result = client.session.call("mail_registered_participants", 35239) + +# Get the raw response from remind_participants +result = client.session.call("remind_participants", 35239) # end example diff --git a/docker-compose.yml b/docker-compose.yml index 146fbd71..fb2bff36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,10 +26,10 @@ services: EMAIL_SMTPPASSWORD: ${LS_SMTP_PASSWORD:-secret} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/index.php/admin"] - interval: 30s + interval: 15s timeout: 10s retries: 3 - start_period: 15s + start_period: 5s db: image: postgres:16 @@ -43,8 +43,8 @@ services: - 5432:5432 healthcheck: test: ["CMD", "pg_isready", "-U", "limesurvey"] - interval: 30s - timeout: 30s + interval: 15s + timeout: 10s retries: 3 storage: @@ -57,6 +57,7 @@ services: command: server /data --console-address ":9001" profiles: - web + - notebook mailhog: image: mailhog/mailhog diff --git a/docs/conf.py b/docs/conf.py index e47d7dd7..e7ca5f8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,18 +13,25 @@ if t.TYPE_CHECKING: from sphinx.application import Sphinx +# -- Project information --------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + project = "citric" + author = "Edgar Ramírez Mondragón" +project_copyright = f"2020, {author}" version = citric.__version__ release = citric.__version__ -project_copyright = f"2020, {author}" + +# -- General configuration ------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.linkcode", "sphinx.ext.napoleon", - "sphinx_autodoc_typehints", "autoapi.extension", "myst_parser", "sphinx_copybutton", @@ -33,30 +40,29 @@ "notfound.extension", ] -myst_heading_anchors = 2 +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} -autodoc_typehints = "description" -autodoc_typehints_description_target = "documented" +nitpicky = True +nitpick_ignore = { + ("py:class", "citric.types.Result"), + ("py:class", "Result"), + ("py:class", "YesNo"), + ("py:class", "T"), + ("py:obj", "T"), +} -autoapi_type = "python" -autoapi_root = "_api" -autoapi_dirs = [ - Path("../src").resolve(), -] -autoapi_options = [ - "members", - "undoc-members", - "show-inheritance", - "show-module-summary", - "special-members", - "imported-members", - "private-members", -] +# -- Options for internationalization -------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-internationalization + +# -- Options for Math ------------------------------------------------------------------ +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-math + +# -- Options for HTML output ----------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_extra_path = [ - "googled10b55fb460af091.html", - "code.png", -] html_theme = "furo" html_theme_options = { "navigation_with_keys": True, @@ -65,46 +71,47 @@ "source_directory": "docs/", } html_title = "Citric, a Python client for LimeSurvey" - -hoverxref_default_type = "tooltip" - -intersphinx_mapping = { - "requests": ("https://requests.readthedocs.io/en/latest/", None), - "requests-cache": ("https://requests-cache.readthedocs.io/en/stable/", None), - "python": ("https://docs.python.org/3/", None), -} - -hoverxref_intersphinx = [ - "requests", +html_extra_path = [ + "googled10b55fb460af091.html", + "code.png", ] -hoverxref_domains = [ - "py", -] +# -- Options for Autodoc --------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration -hoverxref_role_types = { - "hoverxref": "tooltip", - "ref": "modal", - "mod": "modal", - "class": "tooltip", -} +autodoc_typehints = "description" +autodoc_typehints_description_target = "documented" + +# -- Options for extlinks -------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html extlinks = { "rpc_method": ( - "https://api.limesurvey.org/classes/remotecontrol_handle.html#method_%s", + "https://api.limesurvey.org/classes/remotecontrol-handle.html#method_%s", "RPC method %s", ), "ls_manual": ( "https://manual.limesurvey.org/%s", "%s", ), + "ls_tag": ( + "https://github.com/LimeSurvey/LimeSurvey/releases/tag/%s", + "%s", + ), } -source_suffix = { - ".rst": "restructuredtext", - ".md": "markdown", +# -- Options for intersphinx ----------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration + +intersphinx_mapping = { + "requests": ("https://requests.readthedocs.io/en/latest/", None), + "requests-cache": ("https://requests-cache.readthedocs.io/en/stable/", None), + "python": ("https://docs.python.org/3/", None), } +# -- Options for linkcode -------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/linkcode.html#configuration + def linkcode_resolve(domain: str, info: dict) -> str | None: """Get URL to source code. @@ -124,6 +131,49 @@ def linkcode_resolve(domain: str, info: dict) -> str | None: return f"https://github.com/edgarrmondragon/citric/tree/main/src/{filename}.py" +# -- Options for Napoleon -------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html#configuration + +# -- Options for AutoAPI --------------------------------------------------------------- +# https://sphinx-autoapi.readthedocs.io/en/latest/reference/config.html + +autoapi_dirs = [ + Path("../src").resolve(), +] +autoapi_options = [ + "members", + "undoc-members", + "show-inheritance", + "show-module-summary", + "special-members", + "imported-members", + "private-members", +] +autoapi_root = "_api" + +# -- Options for Myst ------------------------------------------------------------------ +# https://myst-parser.readthedocs.io/en/latest/configuration.html + +myst_heading_anchors = 2 + +# -- Options for hoverxref ------------------------------------------------------------- +# https://sphinx-hoverxref.readthedocs.io/en/latest/configuration.html + +hoverxref_role_types = { + "hoverxref": "tooltip", + "ref": "modal", + "mod": "modal", + "class": "tooltip", +} +hoverxref_default_type = "tooltip" +hoverxref_domains = [ + "py", +] +hoverxref_intersphinx = [ + "requests", +] + + def skip_member_filter( app: Sphinx, # noqa: ARG001 what: str, # noqa: ARG001 diff --git a/docs/contributing/environment.md b/docs/contributing/environment.md index 7e431fe8..441a22f5 100644 --- a/docs/contributing/environment.md +++ b/docs/contributing/environment.md @@ -14,8 +14,6 @@ Ready to contribute? Here's how to set up `citric` for local development. cd citric ``` -1. Install [`hatch`][hatch]: - 1. Install [`nox`][nox] (used for automation): ```shell @@ -54,7 +52,6 @@ Ready to contribute? Here's how to set up `citric` for local development. changie new ``` -[hatch]: https://hatch.pypa.io/latest/install/ [nox]: https://nox.thea.codes/en/stable/ [pre-commit]: https://pre-commit.com/ [changie]: https://changie.dev/ diff --git a/docs/contributing/update-github-actions.md b/docs/contributing/update-github-actions.md new file mode 100644 index 00000000..bac8f41f --- /dev/null +++ b/docs/contributing/update-github-actions.md @@ -0,0 +1,25 @@ +# Update GitHub Actions pins + +There are a few ways to update the GitHub Actions pins. + +## Run workflow in GitHub + +Go to the [Actions tab](https://github.com/edgarrmondragon/citric/actions/workflows/gha-update.yml) and click on the `Run workflow` dropdown. + +## Run workflow locally + +1. Install the [GitHub CLI](https://cli.github.com/). +2. Run the following command: + + ```bash + gh workflow run gha-update.yml + ``` + +## Run the `gha-update` tool locally + +1. Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/). +2. Run the following command: + + ```bash + uvx gha-update + ``` diff --git a/docs/how-to.md b/docs/how-to.md index 50b35aa7..a3f95aaf 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -39,7 +39,7 @@ Otherwise, you can manually close the session with {meth}`client.close() ` are supported by the +{ls_manual}`different plugins ` are supported using the `auth_plugin` argument. ```{literalinclude} ../code_samples/auth_plugin.py @@ -72,7 +72,7 @@ Common plugins are `Authdb` (default), `AuthLDAP` and `Authwebserver`. ## Use the session attribute for low-level interaction -This library doesn't (yet) implement all RPC methods, so if you're in dire need of using a method not currently supported, you can use the `session` attribute to invoke the underlying RPC interface without having to pass a session key explicitly: +This library doesn't implement all RPC methods, so if you're in dire need of using a method not currently supported, you can use the `session` attribute to invoke the underlying RPC interface without having to pass a session key explicitly: ```{literalinclude} ../code_samples/session_attr.py :start-after: start example diff --git a/docs/index.md b/docs/index.md index 69dbed62..f370b4e2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,6 +18,19 @@ Release **v{sub-ref}`version`**. ([What's new?](./changelog.md)) :end-before: ``` +### Integration tests + +Integration tests are run against a LimeSurvey instance, and both PostgreSQL and MySQL backends, using Docker Compose. The following versions of LimeSurvey were tested for this release: + +- {ls_tag}`6.5.9+240521` +- {ls_tag}`6.5.7+240515` +- {ls_tag}`6.5.5+240429` +- {ls_tag}`5.6.65+240522` +- {ls_tag}`5.6.63+240508` +- {ls_tag}`5.6.61+240430` + +But also, the latest 5.x and 6.x are tested continuously and are expected to work. + ## How-to guides - [Automatically close the session with a context manager](how-to.md#automatically-close-the-session-with-a-context-manager) @@ -25,7 +38,7 @@ Release **v{sub-ref}`version`**. ([What's new?](./changelog.md)) - [Export responses to a `pandas` dataframe](how-to.md#export-responses-to-a-pandas-dataframe) - [Export responses to a DuckDB database and analyze with SQL](how-to.md#export-responses-to-a-duckdb-database-and-analyze-with-sql) - [Change the default HTTP session attributes](how-to.md#change-the-default-http-session-attributes) -- [Use custom `requests` session](how-to.md#use-custom-requests-session) +- [Use a custom `requests` session](how-to.md#use-a-custom-requests-session) - [Use a different authentication plugin](how-to.md#use-a-different-authentication-plugin) - [Get files uploaded to a survey and move them to S3](how-to.md#get-files-uploaded-to-a-survey-and-move-them-to-s3) - [Use the raw `RPC.session` for low-level interaction](how-to.md#use-the-session-attribute-for-low-level-interaction) @@ -85,4 +98,5 @@ contributing/docs contributing/docker contributing/release contributing/unreleased-features +contributing/update-github-actions ``` diff --git a/docs/notebooks/duckdb.ipynb b/docs/notebooks/duckdb.ipynb index 15ec56e1..8a067d59 100644 --- a/docs/notebooks/duckdb.ipynb +++ b/docs/notebooks/duckdb.ipynb @@ -11,19 +11,9 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Note: you may need to restart the kernel to use updated packages.\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], + "outputs": [], "source": [ - "%pip install --upgrade pip duckdb-engine faker sqlalchemy jupysql --quiet\n", - "%pip install ../.. --quiet # or pip install citric" + "!pip install --upgrade pip citric duckdb-engine faker jupysql sqlalchemy --quiet" ] }, { @@ -147,7 +137,12 @@ } ], "source": [ - "client.activate_survey(survey_id)\n", + "client.activate_survey(\n", + " survey_id,\n", + " user_activation_settings={\n", + " \"datestamp\": True,\n", + " },\n", + ")\n", "client.activate_tokens(survey_id)\n", "\n", "result = client.add_responses(survey_id, fake_responses)\n", @@ -168,7 +163,12 @@ "outputs": [], "source": [ "with Path(\"responses.csv\").open(\"wb\") as file:\n", - " file.write(client.export_responses(survey_id, file_format=\"csv\"))" + " file.write(\n", + " client.export_responses(\n", + " survey_id,\n", + " file_format=\"csv\",\n", + " )\n", + " )" ] }, { @@ -186,10 +186,10 @@ { "data": { "text/html": [ - "Found pyproject.toml from '/Users/edgarramirez/Code/edgarrmondragon/citric'" + "The 'toml' package isn't installed. To load settings from pyproject.toml or ~/.jupysql/config, install with: pip install toml" ], "text/plain": [ - "Found pyproject.toml from '/Users/edgarramirez/Code/edgarrmondragon/citric'" + "The 'toml' package isn't installed. To load settings from pyproject.toml or ~/.jupysql/config, install with: pip install toml" ] }, "metadata": {}, @@ -204,7 +204,20 @@ "cell_type": "code", "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "Connecting to 'duckdb://'" + ], + "text/plain": [ + "Connecting to 'duckdb://'" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "%sql duckdb://" ] @@ -237,8 +250,7 @@ " \n", "
\n", - "ResultSet : to convert to pandas, call .DataFrame() or to polars, call .PolarsDataFrame()
" + "" ], "text/plain": [ "+-------+\n", @@ -294,7 +306,6 @@ " token\n", " startdate\n", " datestamp\n", - " ipaddr\n", " G01Q01\n", " G01Q02\n", " G02Q03\n", @@ -304,93 +315,87 @@ " \n", " \n", " 1\n", - " 2023-08-07 01:01:39\n", + " 2024-02-04 08:26:31.140133\n", " None\n", " en\n", " None\n", - " b468e\n", - " 2023-08-01 21:15:42\n", - " 2023-08-08 19:56:29\n", - " 182.63.148.172\n", - " Him walk white senior win send contain.\n", + " d360f\n", + " 2024-02-01 17:42:02.678612\n", + " 2024-02-08 17:19:08\n", + " Should check nor will. Every difficult thousand vote provide information nice.\n", " 3\n", " None\n", " None\n", " \n", " \n", " 2\n", - " 2023-08-06 13:53:54\n", + " 2024-02-05 10:16:08.647063\n", " None\n", " en\n", " None\n", - " 9bc69\n", - " 2023-08-03 12:59:20\n", - " 2023-08-08 19:56:29\n", - " 53.181.76.129\n", - " Perform bar evidence hour inside safe.\n", - " 3\n", + " c164f\n", + " 2024-02-03 07:43:22.637163\n", + " 2024-02-08 17:19:09\n", + " Dog tree rise born career amount. Few energy market since method could film.\n", + " 4\n", " None\n", " None\n", " \n", " \n", " 3\n", - " 2023-08-06 01:52:39\n", + " 2024-02-07 20:59:08.821572\n", " None\n", " en\n", " None\n", - " e1c82\n", - " 2023-08-03 09:37:28\n", - " 2023-08-08 19:56:29\n", - " 4.124.173.51\n", - " Win weight remain tough factor thing none. Within boy country develop no sure dream.\n", - " 4\n", + " 2730f\n", + " 2024-02-06 15:59:43.136551\n", + " 2024-02-08 17:19:09\n", + " Set study station ten. Responsibility early better big how which. Him truth art.\n", + " 3\n", " None\n", " None\n", " \n", " \n", " 4\n", - " 2023-08-07 00:51:37\n", + " 2024-02-07 19:26:01.977188\n", " None\n", " en\n", " None\n", - " f3c0c\n", - " 2023-08-04 19:39:18\n", - " 2023-08-08 19:56:29\n", - " 155.33.200.9\n", - " Within far deep. Need however he purpose reach. Why one adult return create such spring he.\n", - " 2\n", + " 43736\n", + " 2024-02-07 04:15:00.716972\n", + " 2024-02-08 17:19:09\n", + " Vote see per former message present. Apply drop citizen site yard image group window.\n", + " 5\n", " None\n", " None\n", " \n", " \n", " 5\n", - " 2023-08-03 05:06:15\n", + " 2024-02-03 00:05:17.846386\n", " None\n", " en\n", " None\n", - " 93832\n", - " 2023-08-01 07:00:08\n", - " 2023-08-08 19:56:29\n", - " 163.127.182.180\n", - " Level Democrat under smile check.\n", - " 4\n", + " bfd64\n", + " 2024-02-01 01:10:01.432302\n", + " 2024-02-08 17:19:09\n", + " Truth woman hot much official rather old. Few paper song yard woman likely.\n", + " 3\n", " None\n", " None\n", " \n", " \n", - "\n", - "ResultSet : to convert to pandas, call .DataFrame() or to polars, call .PolarsDataFrame()
" + "" ], "text/plain": [ - "+----+---------------------+----------+---------------+------+-------+---------------------+---------------------+-----------------+---------------------------------------------------------------------------------------------+--------+--------+-------------------+\n", - "| id | submitdate | lastpage | startlanguage | seed | token | startdate | datestamp | ipaddr | G01Q01 | G01Q02 | G02Q03 | G02Q03[filecount] |\n", - "+----+---------------------+----------+---------------+------+-------+---------------------+---------------------+-----------------+---------------------------------------------------------------------------------------------+--------+--------+-------------------+\n", - "| 1 | 2023-08-07 01:01:39 | None | en | None | b468e | 2023-08-01 21:15:42 | 2023-08-08 19:56:29 | 182.63.148.172 | Him walk white senior win send contain. | 3 | None | None |\n", - "| 2 | 2023-08-06 13:53:54 | None | en | None | 9bc69 | 2023-08-03 12:59:20 | 2023-08-08 19:56:29 | 53.181.76.129 | Perform bar evidence hour inside safe. | 3 | None | None |\n", - "| 3 | 2023-08-06 01:52:39 | None | en | None | e1c82 | 2023-08-03 09:37:28 | 2023-08-08 19:56:29 | 4.124.173.51 | Win weight remain tough factor thing none. Within boy country develop no sure dream. | 4 | None | None |\n", - "| 4 | 2023-08-07 00:51:37 | None | en | None | f3c0c | 2023-08-04 19:39:18 | 2023-08-08 19:56:29 | 155.33.200.9 | Within far deep. Need however he purpose reach. Why one adult return create such spring he. | 2 | None | None |\n", - "| 5 | 2023-08-03 05:06:15 | None | en | None | 93832 | 2023-08-01 07:00:08 | 2023-08-08 19:56:29 | 163.127.182.180 | Level Democrat under smile check. | 4 | None | None |\n", - "+----+---------------------+----------+---------------+------+-------+---------------------+---------------------+-----------------+---------------------------------------------------------------------------------------------+--------+--------+-------------------+" + "+----+----------------------------+----------+---------------+------+-------+----------------------------+---------------------+---------------------------------------------------------------------------------------+--------+--------+-------------------+\n", + "| id | submitdate | lastpage | startlanguage | seed | token | startdate | datestamp | G01Q01 | G01Q02 | G02Q03 | G02Q03[filecount] |\n", + "+----+----------------------------+----------+---------------+------+-------+----------------------------+---------------------+---------------------------------------------------------------------------------------+--------+--------+-------------------+\n", + "| 1 | 2024-02-04 08:26:31.140133 | None | en | None | d360f | 2024-02-01 17:42:02.678612 | 2024-02-08 17:19:08 | Should check nor will. Every difficult thousand vote provide information nice. | 3 | None | None |\n", + "| 2 | 2024-02-05 10:16:08.647063 | None | en | None | c164f | 2024-02-03 07:43:22.637163 | 2024-02-08 17:19:09 | Dog tree rise born career amount. Few energy market since method could film. | 4 | None | None |\n", + "| 3 | 2024-02-07 20:59:08.821572 | None | en | None | 2730f | 2024-02-06 15:59:43.136551 | 2024-02-08 17:19:09 | Set study station ten. Responsibility early better big how which. Him truth art. | 3 | None | None |\n", + "| 4 | 2024-02-07 19:26:01.977188 | None | en | None | 43736 | 2024-02-07 04:15:00.716972 | 2024-02-08 17:19:09 | Vote see per former message present. Apply drop citizen site yard image group window. | 5 | None | None |\n", + "| 5 | 2024-02-03 00:05:17.846386 | None | en | None | bfd64 | 2024-02-01 01:10:01.432302 | 2024-02-08 17:19:09 | Truth woman hot much official rather old. Few paper song yard woman likely. | 3 | None | None |\n", + "+----+----------------------------+----------+---------------+------+-------+----------------------------+---------------------+---------------------------------------------------------------------------------------+--------+--------+-------------------+" ] }, "execution_count": 11, @@ -443,33 +448,32 @@ " \n", " \n", " 2\n", - " 16\n", + " 15\n", " \n", " \n", " 3\n", - " 23\n", + " 26\n", " \n", " \n", " 4\n", - " 24\n", + " 19\n", " \n", " \n", " 5\n", - " 17\n", + " 20\n", " \n", " \n", - "\n", - "ResultSet : to convert to pandas, call .DataFrame() or to polars, call .PolarsDataFrame()
" + "" ], "text/plain": [ "+--------+-------+\n", "| G01Q02 | TOTAL |\n", "+--------+-------+\n", "| 1 | 20 |\n", - "| 2 | 16 |\n", - "| 3 | 23 |\n", - "| 4 | 24 |\n", - "| 5 | 17 |\n", + "| 2 | 15 |\n", + "| 3 | 26 |\n", + "| 4 | 19 |\n", + "| 5 | 20 |\n", "+--------+-------+" ] }, @@ -518,65 +522,65 @@ " \n", " \n", " \n", - " 44288\n", - " 5 days, 18:58:30\n", + " 01a8a\n", + " 7 days, 2:47:48.802061\n", " \n", " \n", - " 70c3e\n", - " 5 days, 8:48:22\n", + " 8462d\n", + " 6 days, 14:02:59.801133\n", " \n", " \n", - " 69491\n", - " 5 days, 8:01:47\n", + " 9a07a\n", + " 5 days, 14:45:12.970604\n", " \n", " \n", - " b468e\n", - " 5 days, 3:45:57\n", + " aa7b1\n", + " 5 days, 10:55:18.878968\n", " \n", " \n", - " fde2a\n", - " 4 days, 22:14:05\n", + " 65aaf\n", + " 5 days, 7:17:34.454648\n", " \n", " \n", - " 42bc4\n", - " 4 days, 19:00:15\n", + " d2b4f\n", + " 5 days, 1:28:12.931126\n", " \n", " \n", - " 9c08f\n", - " 4 days, 17:08:00\n", + " 137cd\n", + " 4 days, 21:42:49.472567\n", " \n", " \n", - " bcade\n", - " 4 days, 12:04:27\n", + " 4398c\n", + " 4 days, 18:26:43.167388\n", " \n", " \n", - " 0bcd7\n", - " 4 days, 8:41:31\n", + " 332f3\n", + " 4 days, 13:44:53.925752\n", " \n", " \n", - " 993ee\n", - " 4 days, 7:21:23\n", + " b6750\n", + " 4 days, 9:41:03.360606\n", " \n", " \n", "\n", - "ResultSet : to convert to pandas, call .DataFrame() or to polars, call .PolarsDataFrame()
\n", - "Truncated to displaylimit of 10
If you want to see more, please visit displaylimit configuration" + "Truncated to displaylimit of 10." ], "text/plain": [ - "+-------+------------------+\n", - "| token | duration |\n", - "+-------+------------------+\n", - "| 44288 | 5 days, 18:58:30 |\n", - "| 70c3e | 5 days, 8:48:22 |\n", - "| 69491 | 5 days, 8:01:47 |\n", - "| b468e | 5 days, 3:45:57 |\n", - "| fde2a | 4 days, 22:14:05 |\n", - "| 42bc4 | 4 days, 19:00:15 |\n", - "| 9c08f | 4 days, 17:08:00 |\n", - "| bcade | 4 days, 12:04:27 |\n", - "| 0bcd7 | 4 days, 8:41:31 |\n", - "| 993ee | 4 days, 7:21:23 |\n", - "+-------+------------------+" + "+-------+-------------------------+\n", + "| token | duration |\n", + "+-------+-------------------------+\n", + "| 01a8a | 7 days, 2:47:48.802061 |\n", + "| 8462d | 6 days, 14:02:59.801133 |\n", + "| 9a07a | 5 days, 14:45:12.970604 |\n", + "| aa7b1 | 5 days, 10:55:18.878968 |\n", + "| 65aaf | 5 days, 7:17:34.454648 |\n", + "| d2b4f | 5 days, 1:28:12.931126 |\n", + "| 137cd | 4 days, 21:42:49.472567 |\n", + "| 4398c | 4 days, 18:26:43.167388 |\n", + "| 332f3 | 4 days, 13:44:53.925752 |\n", + "| b6750 | 4 days, 9:41:03.360606 |\n", + "+-------+-------------------------+\n", + "Truncated to displaylimit of 10." ] }, "execution_count": 13, @@ -611,7 +615,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/notebooks/import_s3.ipynb b/docs/notebooks/import_s3.ipynb index 6a09153c..33b7d66c 100644 --- a/docs/notebooks/import_s3.ipynb +++ b/docs/notebooks/import_s3.ipynb @@ -15,24 +15,9 @@ "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: pip in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (22.0.3)\n", - "Requirement already satisfied: boto3 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (1.20.49)\n", - "Requirement already satisfied: s3transfer<0.6.0,>=0.5.0 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from boto3) (0.5.1)\n", - "Requirement already satisfied: jmespath<1.0.0,>=0.7.1 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from boto3) (0.10.0)\n", - "Requirement already satisfied: botocore<1.24.0,>=1.23.49 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from boto3) (1.23.49)\n", - "Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from botocore<1.24.0,>=1.23.49->boto3) (2.8.2)\n", - "Requirement already satisfied: urllib3<1.27,>=1.25.4 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from botocore<1.24.0,>=1.23.49->boto3) (1.26.8)\n", - "Requirement already satisfied: six>=1.5 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from python-dateutil<3.0.0,>=2.1->botocore<1.24.0,>=1.23.49->boto3) (1.16.0)\n" - ] - } - ], + "outputs": [], "source": [ - "!pip install --upgrade pip boto3" + "!pip install --upgrade pip boto3 citric --quiet" ] }, { @@ -44,7 +29,6 @@ "source": [ "import io\n", "import logging\n", - "import os\n", "\n", "import boto3\n", "from IPython.display import HTML\n", @@ -52,27 +36,6 @@ "import citric" ] }, - { - "cell_type": "code", - "execution_count": 3, - "id": "2598031b-31be-4975-b2db-0ebb20d87b28", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.0.8'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "citric.__version__" - ] - }, { "cell_type": "markdown", "id": "fd7cf1aa-88fc-4cb6-9dac-ab5a37fd947a", @@ -85,7 +48,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "c818e679-6a88-40c0-8470-69083f074a0a", "metadata": {}, "outputs": [], @@ -100,18 +63,6 @@ "logger.setLevel(logging.DEBUG)" ] }, - { - "cell_type": "code", - "execution_count": 5, - "id": "97821189", - "metadata": {}, - "outputs": [], - "source": [ - "LS_URL = os.environ[\"LIMESURVEY_URL\"]\n", - "LS_USERNAME = os.environ[\"LIMESURVEY_USERNAME\"]\n", - "LS_PASSWORD = os.environ[\"LIMESURVEY_PASSWORD\"]" - ] - }, { "cell_type": "markdown", "id": "739b1813-7516-4899-b714-126d05f65174", @@ -126,7 +77,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "f45aa557-778a-426b-b227-e5418b9ab719", "metadata": {}, "outputs": [ @@ -134,7 +85,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "free_text.lsq limesurvey_survey_432535.lss survey.lss\n" + "free_text.lsq limesurvey_survey_432535.lss\n", + "group.lsg survey.lss\n" ] } ], @@ -144,13 +96,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "622990b0-6e1c-4431-b7e1-02a7bc2688a4", "metadata": {}, "outputs": [], "source": [ - "s3 = boto3.client(\"s3\", endpoint_url=\"http://storage:9000\")\n", + "s3 = boto3.client(\"s3\")\n", "\n", + "# use your own bucket name here\n", "s3.create_bucket(Bucket=\"testing\")\n", "s3.upload_file(\"../../examples/survey.lss\", \"testing\", \"survey.lss\")" ] @@ -170,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "5e7307f5-b872-4468-baaf-13eff990d345", "metadata": {}, "outputs": [ @@ -178,15 +131,15 @@ "name": "stderr", "output_type": "stream", "text": [ - "2022-02-05 23:38:07,086 INFO Invoked RPC method get_session_key with ID 321389\n", - "2022-02-05 23:38:18,549 INFO Invoked RPC method import_survey with ID 306084\n", - "2022-02-05 23:38:19,266 INFO Invoked RPC method list_questions with ID 478053\n" + "2024-02-08 11:25:12,778 INFO Invoked RPC method get_session_key with ID 119194\n", + "2024-02-08 11:25:13,035 INFO Invoked RPC method import_survey with ID 878109\n", + "2024-02-08 11:25:13,108 INFO Invoked RPC method list_questions with ID 942978\n" ] }, { "data": { "text/html": [ - "

Text for second question


" + "Text for first questionThis is a question help text.
" ], "text/plain": [ "" @@ -198,7 +151,7 @@ { "data": { "text/html": [ - "

Text for first question


" + "Text for second question
" ], "text/plain": [ "" @@ -210,7 +163,7 @@ { "data": { "text/html": [ - "

Please upload a text file

A file with .txt extension


" + "Please upload a text fileA file with .txt
" ], "text/plain": [ "" @@ -223,12 +176,17 @@ "name": "stderr", "output_type": "stream", "text": [ - "2022-02-05 23:38:19,840 INFO Invoked RPC method release_session_key with ID 522063\n" + "2024-02-08 11:25:13,161 INFO Invoked RPC method release_session_key with ID 948663\n" ] } ], "source": [ - "with citric.RPC(LS_URL, LS_USERNAME, LS_PASSWORD) as client:\n", + "# Use your own server's parameters here\n", + "with citric.RPC(\n", + " \"http://localhost:8001/index.php/admin/remotecontrol\",\n", + " \"iamadmin\",\n", + " \"secret\",\n", + ") as client:\n", " file_object = io.BytesIO()\n", " s3.download_fileobj(\"testing\", \"survey.lss\", file_object)\n", "\n", @@ -253,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "id": "0be47c6a-d219-4828-b92d-8f5f6763a4ca", "metadata": {}, "outputs": [ @@ -261,9 +219,9 @@ "name": "stderr", "output_type": "stream", "text": [ - "2022-02-05 23:38:24,724 INFO Invoked RPC method get_session_key with ID 24642\n", - "2022-02-05 23:38:25,890 INFO Invoked RPC method delete_survey with ID 239814\n", - "2022-02-05 23:38:26,447 INFO Invoked RPC method release_session_key with ID 154651\n" + "2024-02-08 11:25:13,291 INFO Invoked RPC method get_session_key with ID 500759\n", + "2024-02-08 11:25:13,379 INFO Invoked RPC method delete_survey with ID 409998\n", + "2024-02-08 11:25:13,430 INFO Invoked RPC method release_session_key with ID 315195\n" ] } ], @@ -271,7 +229,11 @@ "s3.delete_object(Bucket=\"testing\", Key=\"survey.lss\")\n", "s3.delete_bucket(Bucket=\"testing\")\n", "\n", - "with citric.RPC(LS_URL, LS_USERNAME, LS_PASSWORD) as client:\n", + "with citric.RPC(\n", + " \"http://localhost:8001/index.php/admin/remotecontrol\",\n", + " \"iamadmin\",\n", + " \"secret\",\n", + ") as client:\n", " client.delete_survey(survey_id)" ] } @@ -292,7 +254,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/notebooks/pandas_sqlite.ipynb b/docs/notebooks/pandas_sqlite.ipynb index 05da0950..9a90dc2f 100644 --- a/docs/notebooks/pandas_sqlite.ipynb +++ b/docs/notebooks/pandas_sqlite.ipynb @@ -10,36 +10,31 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: pip in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (22.0.3)\n", - "Requirement already satisfied: pandas in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (1.4.0)\n", - "Requirement already satisfied: faker in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (12.1.0)\n", - "Requirement already satisfied: sqlalchemy in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (1.4.31)\n", - "Requirement already satisfied: python-dateutil>=2.8.1 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from pandas) (2.8.2)\n", - "Requirement already satisfied: numpy>=1.18.5 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from pandas) (1.22.2)\n", - "Requirement already satisfied: pytz>=2020.1 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from pandas) (2021.3)\n", - "Requirement already satisfied: greenlet!=0.4.17 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from sqlalchemy) (1.1.2)\n", - "Requirement already satisfied: six>=1.5 in /root/.cache/pypoetry/virtualenvs/citric-9TtSrW0h-py3.9/lib/python3.9/site-packages (from python-dateutil>=2.8.1->pandas) (1.16.0)\n" - ] - } - ], + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [ + "remove-output" + ] + }, + "outputs": [], "source": [ - "!pip install --upgrade pip pandas faker sqlalchemy" + "!pip install --upgrade citric pip pandas faker sqlalchemy --quiet" ] }, { "cell_type": "code", "execution_count": 2, - "metadata": {}, + "metadata": { + "tags": [ + "remove-output" + ] + }, "outputs": [], "source": [ "import io\n", - "import os\n", "from pathlib import Path\n", "\n", "import pandas as pd\n", @@ -49,17 +44,6 @@ "import citric" ] }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "LS_URL = os.environ[\"LIMESURVEY_URL\"]\n", - "LS_USERNAME = os.environ[\"LIMESURVEY_USERNAME\"]\n", - "LS_PASSWORD = os.environ[\"LIMESURVEY_PASSWORD\"]" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -69,11 +53,16 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "client = citric.RPC(LS_URL, LS_USERNAME, LS_PASSWORD)" + "# Use your own server's parameters here\n", + "client = citric.RPC(\n", + " \"http://localhost:8001/index.php/admin/remotecontrol\",\n", + " \"iamadmin\",\n", + " \"secret\",\n", + ")" ] }, { @@ -85,7 +74,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -102,13 +91,18 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "faker = Faker()\n", "\n", - "client.activate_survey(survey_id)\n", + "client.activate_survey(\n", + " survey_id,\n", + " user_activation_settings={\n", + " \"datestamp\": True,\n", + " },\n", + ")\n", "client.activate_tokens(survey_id)\n", "\n", "data = [\n", @@ -124,6 +118,25 @@ "result = client.add_responses(survey_id, data)" ] }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "with io.BytesIO() as file:\n", + " file.write(client.export_responses(survey_id, file_format=\"csv\"))\n", + " file.seek(0)\n", + " responses_df = pd.read_csv(\n", + " file,\n", + " delimiter=\";\",\n", + " parse_dates=[\"datestamp\", \"startdate\", \"submitdate\"],\n", + " index_col=\"id\",\n", + " )\n", + "\n", + "engine = create_engine(\"sqlite:///responses.db\")" + ] + }, { "cell_type": "code", "execution_count": 7, @@ -131,8 +144,159 @@ "outputs": [ { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
submitdatelastpagestartlanguageseedtokenstartdatedatestampG01Q01G01Q02G02Q03G02Q03[filecount]
id
12024-02-08 17:19:28NaNenNaN53afb2024-02-08 17:19:282024-02-08 17:19:28Score soldier network station edge. Degree mil...5NaNNaN
22024-02-08 17:19:28NaNenNaNc36b32024-02-08 17:19:282024-02-08 17:19:28Half college hospital. Sell matter two phone r...3NaNNaN
32024-02-08 17:19:28NaNenNaNf99a12024-02-08 17:19:282024-02-08 17:19:28Sense executive eye five fill. Technology hear...2NaNNaN
42024-02-08 17:19:28NaNenNaNf83112024-02-08 17:19:282024-02-08 17:19:28Score people half. Only center team care radio...2NaNNaN
52024-02-08 17:19:28NaNenNaN676a22024-02-08 17:19:282024-02-08 17:19:28Church clear of. Wear too way I. Expert everyt...5NaNNaN
\n", + "
" + ], "text/plain": [ - "100" + " submitdate lastpage startlanguage seed token \\\n", + "id \n", + "1 2024-02-08 17:19:28 NaN en NaN 53afb \n", + "2 2024-02-08 17:19:28 NaN en NaN c36b3 \n", + "3 2024-02-08 17:19:28 NaN en NaN f99a1 \n", + "4 2024-02-08 17:19:28 NaN en NaN f8311 \n", + "5 2024-02-08 17:19:28 NaN en NaN 676a2 \n", + "\n", + " startdate datestamp \\\n", + "id \n", + "1 2024-02-08 17:19:28 2024-02-08 17:19:28 \n", + "2 2024-02-08 17:19:28 2024-02-08 17:19:28 \n", + "3 2024-02-08 17:19:28 2024-02-08 17:19:28 \n", + "4 2024-02-08 17:19:28 2024-02-08 17:19:28 \n", + "5 2024-02-08 17:19:28 2024-02-08 17:19:28 \n", + "\n", + " G01Q01 G01Q02 G02Q03 \\\n", + "id \n", + "1 Score soldier network station edge. Degree mil... 5 NaN \n", + "2 Half college hospital. Sell matter two phone r... 3 NaN \n", + "3 Sense executive eye five fill. Technology hear... 2 NaN \n", + "4 Score people half. Only center team care radio... 2 NaN \n", + "5 Church clear of. Wear too way I. Expert everyt... 5 NaN \n", + "\n", + " G02Q03[filecount] \n", + "id \n", + "1 NaN \n", + "2 NaN \n", + "3 NaN \n", + "4 NaN \n", + "5 NaN " ] }, "execution_count": 7, @@ -141,18 +305,7 @@ } ], "source": [ - "with io.BytesIO() as file:\n", - " file.write(client.export_responses(survey_id, file_format=\"csv\"))\n", - " file.seek(0)\n", - " responses_df = pd.read_csv(\n", - " file,\n", - " delimiter=\";\",\n", - " parse_dates=[\"datestamp\", \"startdate\", \"submitdate\"],\n", - " index_col=\"id\",\n", - " )\n", - "\n", - "engine = create_engine(\"sqlite:///responses.db\")\n", - "responses_df.to_sql(\"responses\", engine, if_exists=\"replace\")" + "responses_df.head()" ] }, { @@ -163,7 +316,7 @@ { "data": { "text/plain": [ - "{'status': 'OK'}" + "100" ] }, "execution_count": 8, @@ -171,6 +324,26 @@ "output_type": "execute_result" } ], + "source": [ + "responses_df.to_sql(\"responses\", engine, if_exists=\"replace\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'status': 'OK'}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "client.delete_survey(survey_id)" ] @@ -195,7 +368,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.10" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/notebooks/parse_files.ipynb b/docs/notebooks/parse_files.ipynb index 5deebc40..2ef6a6f3 100644 --- a/docs/notebooks/parse_files.ipynb +++ b/docs/notebooks/parse_files.ipynb @@ -15,9 +15,9 @@ "metadata": {}, "outputs": [], "source": [ - "from xml.etree import ElementTree # noqa: S405\n", + "import xml.etree.ElementTree as ET # noqa: S405\n", "\n", - "tree = ElementTree.parse(\"../examples/free_text.lsq\") # noqa: S314" + "tree = ET.parse(\"../../examples/free_text.lsq\") # noqa: S314" ] }, { @@ -331,7 +331,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.10" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/requirements.txt b/docs/requirements.txt index 5a28f96e..5ea7b426 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,119 +1,104 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --extra=docs --output-file=docs/requirements.txt -# -alabaster==0.7.16 +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --universal --pre --python-version 3.12 --extra docs -o docs/requirements.txt +alabaster==1.0.0 # via sphinx -anyascii==0.3.2 +astroid==3.3.2 # via sphinx-autoapi -astroid==3.0.2 - # via sphinx-autoapi -autodocsumm==0.2.12 +autodocsumm==0.2.13 # via citric (pyproject.toml) -babel==2.14.0 +babel==2.16.0 # via sphinx -beautifulsoup4==4.12.3 +beautifulsoup4==4.13.0b2 # via furo -certifi==2023.11.17 +certifi==2024.8.30 # via requests charset-normalizer==3.3.2 # via requests -colorama==0.4.6 - # via sphinx-autobuild -docutils==0.20.1 +colorama==0.4.6 ; sys_platform == 'win32' + # via sphinx +docutils==0.21.2 # via + # citric (pyproject.toml) # myst-parser # sphinx -furo==2023.9.10 +furo==2024.8.6 # via citric (pyproject.toml) -idna==3.6 +idna==3.8 # via requests imagesize==1.4.1 # via sphinx -jinja2==3.1.3 +jinja2==3.1.4 # via # myst-parser # sphinx # sphinx-autoapi -livereload==2.6.3 - # via sphinx-autobuild markdown-it-py==3.0.0 # via # mdit-py-plugins # myst-parser -markupsafe==2.1.4 +markupsafe==2.1.5 # via jinja2 -mdit-py-plugins==0.4.0 +mdit-py-plugins==0.4.1 # via myst-parser mdurl==0.1.2 # via markdown-it-py -myst-parser==2.0.0 +myst-parser==4.0.0 # via citric (pyproject.toml) -packaging==23.2 +packaging==24.1 # via sphinx -pygments==2.17.2 +pygments==2.18.0 # via # furo # sphinx -pyyaml==6.0.1 +pyyaml==6.0.2 # via # myst-parser # sphinx-autoapi -requests==2.31.0 +requests==2.32.3 # via # citric (pyproject.toml) # sphinx -six==1.16.0 - # via livereload snowballstemmer==2.2.0 # via sphinx -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 -sphinx==7.2.6 +sphinx==8.0.2 # via - # autodocsumm # citric (pyproject.toml) + # autodocsumm # furo # myst-parser # sphinx-autoapi - # sphinx-autobuild - # sphinx-autodoc-typehints # sphinx-basic-ng # sphinx-copybutton # sphinx-hoverxref # sphinx-notfound-page # sphinxcontrib-jquery -sphinx-autoapi==3.0.0 - # via citric (pyproject.toml) -sphinx-autobuild==2021.3.14 - # via citric (pyproject.toml) -sphinx-autodoc-typehints==1.25.2 +sphinx-autoapi==3.3.1 # via citric (pyproject.toml) sphinx-basic-ng==1.0.0b2 # via furo sphinx-copybutton==0.5.2 # via citric (pyproject.toml) -sphinx-hoverxref==1.3.0 +sphinx-hoverxref==1.4.0 # via citric (pyproject.toml) -sphinx-notfound-page==1.0.0 +sphinx-notfound-page==1.0.4 # via citric (pyproject.toml) -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.5 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-hoverxref sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -tornado==6.4 - # via livereload -urllib3==2.1.0 +typing-extensions==4.12.2 + # via beautifulsoup4 +urllib3==2.2.2 # via requests diff --git a/docs/rpc_coverage.md b/docs/rpc_coverage.md index 36ca4487..7ecf7c62 100644 --- a/docs/rpc_coverage.md +++ b/docs/rpc_coverage.md @@ -2,61 +2,61 @@ Full list of methods is available at [the Remote Control documentation](https://api.limesurvey.org/classes/remotecontrol_handle.html). -| Name | Implemented | Description | -| :----------------------------- | :-------------------------------------------- | :-------------------------------------------------------------------------------- | -| `activate_survey` | [Yes](citric.RPC.activate_survey) | Activate survey (RPC function) | -| `activate_tokens` | [Yes](citric.RPC.activate_tokens) | Activate survey participants (RPC function) | -| `add_group` | [Yes](citric.RPC.add_group) | Add empty group with minimum details (RPC function) | -| `add_language` | [Yes](citric.RPC.add_language) | Add a survey language (RPC function) | -| `add_participants` | [Yes](citric.RPC.add_participants) | Add participants to the survey. | -| `add_quota` | [Yes](citric.RPC.add_quota) | Add a new quota with minimum details | -| `add_response` | [Yes](citric.RPC.add_response) | Add a response to the survey responses collection. | -| `add_survey` | [Yes](citric.RPC.add_survey) | Add an empty survey with minimum details | -| `copy_survey` | [Yes](citric.RPC.copy_survey) | Copy survey (RPC function) | -| `cpd_importParticipants` | [Yes](citric.RPC.import_cpdb_participants) | Import a participant into the LimeSurvey CPDB | -| `delete_group` | [Yes](citric.RPC.delete_group) | Delete a group from a chosen survey (RPC function) | -| `delete_language` | [Yes](citric.RPC.delete_language) | Delete a language from a survey (RPC function) | -| `delete_participants` | [Yes](citric.RPC.delete_participants) | Delete multiple participants from the survey participants table (RPC function) | -| `delete_question` | [Yes](citric.RPC.delete_question) | Delete question from a survey (RPC function) | -| `delete_quota` | [Yes](citric.RPC.delete_quota) | Delete a quota | -| `delete_response` | [Yes](citric.RPC.delete_response) | Delete a response in a given survey using its Id | -| `delete_survey` | [Yes](citric.RPC.delete_survey) | Delete a survey. | -| `export_responses` | [Yes](citric.RPC.export_responses) | Export responses in base64 encoded string | -| `export_responses_by_token` | [Yes](citric.RPC.export_responses) | Export token response in a survey. | -| `export_statistics` | [Yes](citric.RPC.export_statistics) | Export survey statistics (RPC function) | -| `export_timeline` | [Yes](citric.RPC.export_timeline) | Export submission timeline (RPC function) | -| `get_available_site_settings` | [Yes](citric.RPC.get_available_site_settings) | Get the available site settings | -| `get_fieldmap` | [Yes](citric.RPC.get_fieldmap) | Returns the requested survey's fieldmap in an array | -| `get_group_properties` | [Yes](citric.RPC.get_group_properties) | Get the properties of a group of a survey . | -| `get_language_properties` | [Yes](citric.RPC.get_language_properties) | Get survey language properties (RPC function) | -| `get_participant_properties` | [Yes](citric.RPC.get_participant_properties) | Get settings of a survey participant (RPC function) | -| `get_question_properties` | [Yes](citric.RPC.get_question_properties) | Get properties of a question in a survey. | -| `get_quota_properties` | [Yes](citric.RPC.get_quota_properties) | Get quota attributes (RPC function) | -| `get_response_ids` | [Yes](citric.RPC.get_response_ids) | Find response IDs given a survey ID and a token (RPC function) | -| `get_session_key` | [Yes](Session) | Create and return a session key. | -| `get_site_settings` | [Yes](citric.RPC.get_default_theme) | Get a global setting | -| `get_summary` | [Yes](citric.RPC.get_summary) | Get survey summary, regarding token usage and survey participation (RPC function) | -| `get_survey_properties` | [Yes](citric.RPC.get_survey_properties) | Get survey properties (RPC function) | -| `get_uploaded_files` | [Yes](citric.RPC.get_uploaded_files) | Obtain all uploaded files for all responses | -| `import_group` | [Yes](citric.RPC.import_group) | Import a group and add to a survey (RPC function) | -| `import_question` | [Yes](citric.RPC.import_question) | Import question (RPC function) | -| `import_survey` | [Yes](citric.RPC.import_survey) | Import survey in a known format (RPC function) | -| `invite_participants` | [Yes](citric.RPC.invite_participants) | Invite participants in a survey (RPC function) | -| `list_groups` | [Yes](citric.RPC.list_groups) | Get survey groups (RPC function) | -| `list_participants` | [Yes](citric.RPC.list_participants) | Return the IDs and properties of survey participants (RPC function) | -| `list_questions` | [Yes](citric.RPC.list_questions) | Return the ids and info of (sub-)questions of a survey/group (RPC function) | -| `list_quotas` | [Yes](citric.RPC.list_quotas) | List the quotas in a survey | -| `list_survey_groups` | [Yes](citric.RPC.list_survey_groups) | List the survey groups belonging to a user | -| `list_surveys` | [Yes](citric.RPC.list_surveys) | List the survey belonging to a user (RPC function) | -| `list_users` | [Yes](citric.RPC.list_users) | Get list the ids and info of administration user(s) (RPC function) | -| `mail_registered_participants` | No | Send e-mails to registered participants in a survey (RPC function) | -| `release_session_key` | [Yes](Session.close) | Close the RPC session | -| `remind_participants` | No | Send a reminder to participants in a survey (RPC function) | -| `set_group_properties` | [Yes](citric.RPC.set_group_properties) | Set group properties (RPC function) | -| `set_language_properties` | [Yes](citric.RPC.set_language_properties) | Set survey language properties (RPC function) | -| `set_participant_properties` | [Yes](citric.RPC.set_participant_properties) | Set properties of a survey participant (RPC function) | -| `set_question_properties` | [Yes](citric.RPC.set_question_properties) | Set question properties. | -| `set_quota_properties` | [Yes](citric.RPC.set_quota_properties) | Set quota attributes (RPC function) | -| `set_survey_properties` | [Yes](citric.RPC.set_survey_properties) | Set survey properties (RPC function) | -| `update_response` | [Yes](citric.RPC.update_response) | Update a response in a given survey. | -| `upload_file` | [Yes](citric.RPC.upload_file) | Uploads one file to be used later. | +| Name | Implemented | Description | +| :----------------------------- | :------------------------------------------------------------------ | :-------------------------------------------------------------------------------- | +| `activate_survey` | [Yes](citric.RPC.activate_survey) | Activate survey (RPC function) | +| `activate_tokens` | [Yes](citric.RPC.activate_tokens) | Activate survey participants (RPC function) | +| `add_group` | [Yes](citric.RPC.add_group) | Add empty group with minimum details (RPC function) | +| `add_language` | [Yes](citric.RPC.add_language) | Add a survey language (RPC function) | +| `add_participants` | [Yes](citric.RPC.add_participants) | Add participants to the survey. | +| `add_quota` | [Yes](citric.RPC.add_quota) | Add a new quota with minimum details | +| `add_response` | [Yes](citric.RPC.add_response) | Add a response to the survey responses collection. | +| `add_survey` | [Yes](citric.RPC.add_survey) | Add an empty survey with minimum details | +| `copy_survey` | [Yes](citric.RPC.copy_survey) | Copy survey (RPC function) | +| `cpd_importParticipants` | [Yes](citric.RPC.import_cpdb_participants) | Import a participant into the LimeSurvey CPDB | +| `delete_group` | [Yes](citric.RPC.delete_group) | Delete a group from a chosen survey (RPC function) | +| `delete_language` | [Yes](citric.RPC.delete_language) | Delete a language from a survey (RPC function) | +| `delete_participants` | [Yes](citric.RPC.delete_participants) | Delete multiple participants from the survey participants table (RPC function) | +| `delete_question` | [Yes](citric.RPC.delete_question) | Delete question from a survey (RPC function) | +| `delete_quota` | [Yes](citric.RPC.delete_quota) | Delete a quota | +| `delete_response` | [Yes](citric.RPC.delete_response) | Delete a response in a given survey using its Id | +| `delete_survey` | [Yes](citric.RPC.delete_survey) | Delete a survey. | +| `export_responses` | [Yes](citric.RPC.export_responses) | Export responses in base64 encoded string | +| `export_responses_by_token` | [Yes](citric.RPC.export_responses) | Export token response in a survey. | +| `export_statistics` | [Yes](citric.RPC.export_statistics) | Export survey statistics (RPC function) | +| `export_timeline` | [Yes](citric.RPC.export_timeline) | Export submission timeline (RPC function) | +| `get_available_site_settings` | [Yes](citric.RPC.get_available_site_settings) | Get the available site settings | +| `get_fieldmap` | [Yes](citric.RPC.get_fieldmap) | Returns the requested survey's fieldmap in an array | +| `get_group_properties` | [Yes](citric.RPC.get_group_properties) | Get the properties of a group of a survey . | +| `get_language_properties` | [Yes](citric.RPC.get_language_properties) | Get survey language properties (RPC function) | +| `get_participant_properties` | [Yes](citric.RPC.get_participant_properties) | Get settings of a survey participant (RPC function) | +| `get_question_properties` | [Yes](citric.RPC.get_question_properties) | Get properties of a question in a survey. | +| `get_quota_properties` | [Yes](citric.RPC.get_quota_properties) | Get quota attributes (RPC function) | +| `get_response_ids` | [Yes](citric.RPC.get_response_ids) | Find response IDs given a survey ID and a token (RPC function) | +| `get_session_key` | [Yes](Session) | Create and return a session key. | +| `get_site_settings` | [Yes](citric.RPC.get_default_theme) | Get a global setting | +| `get_summary` | [Yes](citric.RPC.get_summary) | Get survey summary, regarding token usage and survey participation (RPC function) | +| `get_survey_properties` | [Yes](citric.RPC.get_survey_properties) | Get survey properties (RPC function) | +| `get_uploaded_files` | [Yes](citric.RPC.get_uploaded_files) | Obtain all uploaded files for all responses | +| `import_group` | [Yes](citric.RPC.import_group) | Import a group and add to a survey (RPC function) | +| `import_question` | [Yes](citric.RPC.import_question) | Import question (RPC function) | +| `import_survey` | [Yes](citric.RPC.import_survey) | Import survey in a known format (RPC function) | +| `invite_participants` | [Yes](citric.RPC.invite_participants) | Invite participants in a survey (RPC function) | +| `list_groups` | [Yes](citric.RPC.list_groups) | Get survey groups (RPC function) | +| `list_participants` | [Yes](citric.RPC.list_participants) | Return the IDs and properties of survey participants (RPC function) | +| `list_questions` | [Yes](citric.RPC.list_questions) | Return the ids and info of (sub-)questions of a survey/group (RPC function) | +| `list_quotas` | [Yes](citric.RPC.list_quotas) | List the quotas in a survey | +| `list_survey_groups` | [Yes](citric.RPC.list_survey_groups) | List the survey groups belonging to a user | +| `list_surveys` | [Yes](citric.RPC.list_surveys) | List the survey belonging to a user (RPC function) | +| `list_users` | [Yes](citric.RPC.list_users) | Get list the ids and info of administration user(s) (RPC function) | +| `mail_registered_participants` | [No](how-to.md#use-the-session-attribute-for-low-level-interaction) | Send e-mails to registered participants in a survey (RPC function) | +| `release_session_key` | [Yes](Session.close) | Close the RPC session | +| `remind_participants` | [No](how-to.md#use-the-session-attribute-for-low-level-interaction) | Send a reminder to participants in a survey (RPC function) | +| `set_group_properties` | [Yes](citric.RPC.set_group_properties) | Set group properties (RPC function) | +| `set_language_properties` | [Yes](citric.RPC.set_language_properties) | Set survey language properties (RPC function) | +| `set_participant_properties` | [Yes](citric.RPC.set_participant_properties) | Set properties of a survey participant (RPC function) | +| `set_question_properties` | [Yes](citric.RPC.set_question_properties) | Set question properties. | +| `set_quota_properties` | [Yes](citric.RPC.set_quota_properties) | Set quota attributes (RPC function) | +| `set_survey_properties` | [Yes](citric.RPC.set_survey_properties) | Set survey properties (RPC function) | +| `update_response` | [Yes](citric.RPC.update_response) | Update a response in a given survey. | +| `upload_file` | [Yes](citric.RPC.upload_file) | Uploads one file to be used later. | diff --git a/noxfile.py b/noxfile.py index 1646a5d9..ea7b3073 100644 --- a/noxfile.py +++ b/noxfile.py @@ -6,31 +6,40 @@ import shutil from pathlib import Path -from nox import Session, session +import nox GH_ACTIONS_ENV_VAR = "GITHUB_ACTIONS" FORCE_COLOR = "FORCE_COLOR" +nox.options.sessions = [ + "tests", + "xdoctest", + "deps", + "mypy", + "docs-build", + "api", +] +nox.needs_version = ">=2024.4.15" +nox.options.default_venv_backend = "uv|virtualenv" + package = "citric" -python_versions = ["3.13", "3.12", "3.11", "3.10", "3.9", "3.8"] +python_versions = [ + "3.13", + "3.12", + "3.11", + "3.10", + "3.9", + "3.8", +] pypy_versions = ["pypy3.9", "pypy3.10"] all_python_versions = python_versions + pypy_versions -main_cpython_version = "3.12" -main_pypy_version = "pypy3.9" - locations = "src", "tests", "noxfile.py", "docs/conf.py" -@session(python=all_python_versions, tags=["test"]) -def tests(session: Session) -> None: - """Execute pytest tests and compute coverage.""" - session.install(".[tests]") - args = session.posargs or ["-m", "not integration_test"] - - env = {"COVERAGE_CORE": "sysmon"} if session.python in {"3.12", "3.13"} else {} - +def _run_tests(session: nox.Session, *args: str) -> None: + env = {"COVERAGE_CORE": "sysmon"} try: session.run("coverage", "run", "-m", "pytest", *args, env=env) finally: @@ -38,31 +47,29 @@ def tests(session: Session) -> None: session.notify("coverage", posargs=[]) -@session(python=[main_cpython_version, main_pypy_version], tags=["test"]) -def integration(session: Session) -> None: - """Execute integration tests and compute coverage.""" - session.install(".[tests]") +@nox.session(python=python_versions, tags=["test"]) +@nox.parametrize("constraints", ["highest", "lowest-direct"]) +def tests(session: nox.Session, constraints: str) -> None: + """Execute pytest tests and compute coverage.""" + install_args = ["-v", "citric[tests] @ ."] + if constraints == "lowest-direct": + install_args.extend(["-c", "requirements/requirements-lowest-direct.txt"]) - args = [ - "coverage", - "run", - "-m", - "pytest", - "-m", - "integration_test", - ] + session.install(*install_args) + args = session.posargs or ["-m", "not integration_test"] + _run_tests(session, *args) - env = {"COVERAGE_CORE": "sysmon"} if session.python in {"3.12", "3.13"} else {} - try: - session.run(*args, *session.posargs, env=env) - finally: - if session.interactive: - session.notify("coverage", posargs=[]) +@nox.session(tags=["test"]) +def integration(session: nox.Session) -> None: + """Execute integration tests and compute coverage.""" + session.install("-v", "citric[tests] @ .") + args = session.posargs or ["-m", "integration_test"] + _run_tests(session, *args) -@session(python=[main_cpython_version, main_pypy_version], tags=["test"]) -def xdoctest(session: Session) -> None: +@nox.session(tags=["test"]) +def xdoctest(session: nox.Session) -> None: """Run examples with xdoctest.""" if session.posargs: args = [package, *session.posargs] @@ -71,17 +78,17 @@ def xdoctest(session: Session) -> None: if FORCE_COLOR in os.environ: args.append("--colored=1") - session.install(".") - session.install("xdoctest[colors]") + session.install("-v", "citric @ .") + session.install("-v", "xdoctest[colors]") session.run("python", "-m", "xdoctest", *args) -@session() -def coverage(session: Session) -> None: +@nox.session() +def coverage(session: nox.Session) -> None: """Upload coverage data.""" args = session.posargs or ["report"] - session.install("coverage[toml]") + session.install("-v", "coverage[toml]") if not session.posargs and any(Path().glob(".coverage.*")): session.run("coverage", "combine", "--debug=pathmap") @@ -89,30 +96,34 @@ def coverage(session: Session) -> None: session.run("coverage", *args) -@session(name="deps", python=python_versions) -def dependencies(session: Session) -> None: +@nox.session(name="deps", python=python_versions) +def dependencies(session: nox.Session) -> None: """Check issues with dependencies.""" - session.install(".[dev]") - session.install("deptry") - session.run("deptry", "src") + install_env = {} + if session.python in {"3.13", "3.14"}: + install_env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" + + session.install("-v", "citric[dev] @ .", env=install_env) + session.install("-v", "deptry") + session.run("deptry", "src", "tests", "docs") -@session(python=python_versions, tags=["lint"]) -def mypy(session: Session) -> None: +@nox.session(python=python_versions, tags=["lint"]) +def mypy(session: nox.Session) -> None: """Type-check using mypy.""" args = session.posargs or locations - session.install(".[typing]") + session.install("-v", "citric[typing] @ .") session.run("mypy", *args) -@session(name="docs-build") -def docs_build(session: Session) -> None: +@nox.session(name="docs-build") +def docs_build(session: nox.Session) -> None: """Build the documentation.""" args = session.posargs or ["docs", "build"] if not session.posargs and FORCE_COLOR in os.environ: args.insert(0, "--color") - session.install(".[docs]") + session.install("-v", "citric[docs] @ .") build_dir = Path("build") if build_dir.exists(): @@ -121,8 +132,8 @@ def docs_build(session: Session) -> None: session.run("sphinx-build", *args) -@session(name="docs-serve") -def docs_serve(session: Session) -> None: +@nox.session(name="docs-serve") +def docs_serve(session: nox.Session) -> None: """Build the documentation.""" args = session.posargs or [ "--open-browser", @@ -135,7 +146,7 @@ def docs_serve(session: Session) -> None: "docs", "build", ] - session.install(".[docs]") + session.install("-v", "citric[docs] @ .", "sphinx-autobuild") build_dir = Path("build") if build_dir.exists(): @@ -144,8 +155,8 @@ def docs_serve(session: Session) -> None: session.run("sphinx-autobuild", *args) -@session(name="api") -def api_changes(session: Session) -> None: +@nox.session(name="api") +def api_changes(session: nox.Session) -> None: """Check for API changes.""" args = [ "griffe", @@ -160,15 +171,35 @@ def api_changes(session: Session) -> None: session.run(*args, external=True) -@session -def notebook(session: Session) -> None: +@nox.session() +def notebook(session: nox.Session) -> None: """Start a Jupyter notebook.""" - session.install("-e", ".[dev]") - session.run("jupyter", "lab") - - -@session(name="generate-tags", tags=["status"]) -def tags(session: Session) -> None: + session.install( + "boto3", + "duckdb", + "duckdb-engine", + "faker", + "jupysql", + "jupyterlab", + "pandas", + "pyarrow", + "sqlalchemy", + "-e", + ".", + ) + session.run( + "jupyter", + "lab", + env={ + "AWS_ENDPOINT_URL": "http://localhost:9000", + "AWS_ACCESS_KEY_ID": "minioadmin", + "AWS_SECRET_ACCESS_KEY": "minioadmin", + }, + ) + + +@nox.session(name="generate-tags", tags=["status"]) +def tags(session: nox.Session) -> None: """Print tags.""" - session.install("requests", "requests-cache") + session.install("-v", "requests", "requests-cache") session.run("python", "scripts/docker_tags.py") diff --git a/pyproject.toml b/pyproject.toml index 138e1378..2287f9bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,13 @@ keywords = [ "json-rpc", "limesurvey", ] -license = {file = "LICENSE"} -maintainers = [{ name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }] -authors = [{ name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }] +license = { file = "LICENSE" } +maintainers = [ + { name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }, +] +authors = [ + { name = "Edgar Ramírez-Mondragón", email = "edgarrm358@gmail.com" }, +] requires-python = ">=3.8" classifiers = [ "Development Status :: 5 - Production/Stable", @@ -33,6 +37,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", @@ -42,35 +47,33 @@ dynamic = [ "version", ] dependencies = [ - "requests>=2.23", + "requests>=2.25.1", ] optional-dependencies.dev = [ "citric[docs,tests,typing]", "colorama>=0.4.6", - "jupyterlab", + "deptry>=0.12", "requests-cache>=1.1", ] optional-dependencies.docs = [ - "autodocsumm>=0.2.5", # 0.2.4 has a problematic dependency 'Sphinx>=2.2.*' - "furo", - "myst-parser", - "sphinx", - "sphinx-autoapi", - "sphinx-autobuild", - "sphinx-autodoc-typehints", - "sphinx-copybutton", - "sphinx-hoverxref", - "sphinx-notfound-page", + "autodocsumm>=0.2.5", # 0.2.4 has a problematic dependency 'Sphinx>=2.2.*' + "docutils>=0.20", + "furo>=2024.1.29", + "myst-parser>=2", + "sphinx>=7", + "sphinx-autoapi>=3", + "sphinx-copybutton>=0.5.2", + "sphinx-hoverxref>=1.3", + "sphinx-notfound-page>=1", ] optional-dependencies.tests = [ - "coverage[toml]>=7.4", - "deptry>=0.12", + "coverage[toml]>=7.4.2", "faker>=19", - "pytest>=7.3.1", + "pytest>=8", "pytest-github-actions-annotate-failures>=0.1.7", - "pytest-httpserver", - "pytest-reverse", - "pytest-subtests", + "pytest-httpserver>=1.0.8", + "pytest-reverse>=1.7", + "pytest-subtests>=0.11", "python-dotenv>=1", "semver>=3.0.1", "tinydb>=4.8", @@ -78,12 +81,13 @@ optional-dependencies.tests = [ ] optional-dependencies.typing = [ "citric[tests]", - "mypy>=1.8", + "mypy>=1.9", "sphinx", "types-requests>=2.31.0.2", - 'typing-extensions>=4.6; python_version < "3.12"', + "typing-extensions>=4.6; python_version<'3.12'", ] urls.Documentation = "https://citric.readthedocs.io" +urls.Funding = "https://github.com/sponsors/edgarrmondragon" urls.Homepage = 'https://github.com/edgarrmondragon/citric' urls."Issue Tracker" = "https://github.com/edgarrmondragon/citric/issues" urls.Repository = "https://github.com/edgarrmondragon/citric" @@ -92,201 +96,197 @@ urls.Repository = "https://github.com/edgarrmondragon/citric" source = "vcs" [tool.ruff] -include = ["*.py", "*.pyi", "**/pyproject.toml", "*.ipynb"] line-length = 88 -src = ["src", "tests", "docs"] -target-version = "py38" - -[tool.ruff.lint] -explicit-preview-rules = false -ignore = [ - "ANN101", # missing-type-self - "DJ", # flake8-django - "FIX002", # line-contains-todo - "COM812", # missing-trailing-comma - "ISC001", # single-line-implicit-string-concatenation - "D107", # undocumented-public-init -] -preview = true -select = [ - "F", # Pyflakes - "E", # pycodestyle (error) - "W", # pycodestyle (warning) - "C90", # mccabe - "I", # isort - "N", # pep8-naming - "D", # pydocstyle - "UP", # pyupgrade - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "S", # bandit - "BLE", # flake8-blind-except - "FBT", # flake8-boolean-trap - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "EM", # flake8-errmsg - "FA", # flake8-future-annotations - "ISC", # flake8-implicit-str-concat - "ICN", # flake8-import-conventions - "G", # flake8-logging-format - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "PT", # flake8-pytest-style - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SLOT", # flake8-slots - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-type-checking - "ARG", # flake8-unused-arguments - "PTH", # flake8-use-pathlib - "TD", # flake8-todos - "FIX", # flake8-fixme - "ERA", # flake8-eradicate - "PD", # pandas-vet - "PGH", # pygrep-hooks - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint - "TRY", # tryceratops - "FLY", # flynt - "PERF", # perflint - "FURB", # refurb - "LOG", # flake8-logging - "RUF", # Ruff-specific rules -] -unfixable = [ - "ERA", # Don't remove commented out code +src = [ + "docs", + "src", + "tests", ] -[tool.ruff.format] -docstring-code-format = true +include = [ + "**/pyproject.toml", + "*.ipynb", + "*.py", + "*.pyi", +] # Enable preview style formatting. -preview = true - -[tool.ruff.lint.per-file-ignores] -"docs/notebooks/*" = [ +format.preview = true +format.docstring-code-format = true +lint.select = [ + "A", # flake8-builtins + "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + "COM", # flake8-commas + "D", # pydocstyle + "DTZ", # flake8-datetimez + "E", # pycodestyle (error) + "EM", # flake8-errmsg + "ERA", # flake8-eradicate + "F", # Pyflakes + "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + "FIX", # flake8-fixme + "FLY", # flynt + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "LOG", # flake8-logging + "N", # pep8-naming + "PD", # pandas-vet + "PERF", # perflint + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PLC", # pylint + "PLE", # pylint + "PLR", # pylint + "PLW", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "Q", # flake8-quotes + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # bandit + "SIM", # flake8-simplify + "SLF", # flake8-self + "SLOT", # flake8-slots + "T10", # flake8-debugger + "T20", # flake8-print + "TCH", # flake8-type-checking + "TD", # flake8-todos + "TID", # flake8-tidy-imports + "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle (warning) + "YTT", # flake8-2020 +] +lint.ignore = [ + "ANN101", # missing-type-self + "COM812", # missing-trailing-comma + "D107", # undocumented-public-init + "DJ", # flake8-django + "FIX002", # line-contains-todo + "ISC001", # single-line-implicit-string-concatenation +] +lint.explicit-preview-rules = false +lint.per-file-ignores."docs/notebooks/*" = [ "D100", # undocumented-public-module - "INP001", # implicit-namespace-package - "I002", # missing-required-import "E402", # module-import-not-at-top-of-file + "I002", # missing-required-import + "INP001", # implicit-namespace-package ] -"src/*" = [ +lint.per-file-ignores."src/*" = [ "PD", # pandas-vet ] -"tests/*" = [ +lint.per-file-ignores."tests/*" = [ + "ANN201", # missing-return-type-undocumented-public-function "ARG00", # unused-method-argument "C901", # complex-structure + "PLR2004", # magic-value-comparison + "PLR6301", # no-self-use "S101", # assert "S105", # hardcoded-password-string "S106", # hardcoded-password-func-arg - "ANN201", # missing-return-type-undocumented-public-function - "PLR2004", # magic-value-comparison "SLF001", # private-member-access - "PLR6301", # no-self-use ] - -[tool.ruff.lint.flake8-quotes] -docstring-quotes = "double" -inline-quotes = "double" -multiline-quotes = "double" - -[tool.ruff.lint.flake8-annotations] -allow-star-arg-any = true -mypy-init-return = true -suppress-dummy-args = true - -[tool.ruff.lint.flake8-errmsg] -max-string-length = 30 - -[tool.ruff.lint.flake8-import-conventions] -banned-from = ["typing"] - -[tool.ruff.lint.flake8-import-conventions.extend-aliases] -typing = "t" - -[tool.ruff.lint.flake8-pytest-style] -fixture-parentheses = false -mark-parentheses = false - -[tool.ruff.lint.isort] -known-first-party = ["citric"] -required-imports = ["from __future__ import annotations"] - -[tool.ruff.lint.mccabe] -max-complexity = 5 - -[tool.ruff.lint.pydocstyle] -convention = "google" - -[tool.ruff.lint.pylint] -max-args = 10 +lint.unfixable = [ + "ERA", # Don't remove commented out code +] +lint.flake8-annotations.allow-star-arg-any = true +lint.flake8-annotations.mypy-init-return = true +lint.flake8-annotations.suppress-dummy-args = true +lint.flake8-errmsg.max-string-length = 30 +lint.flake8-import-conventions.banned-from = [ + "typing", +] +lint.flake8-import-conventions.extend-aliases.typing = "t" +lint.flake8-quotes.docstring-quotes = "double" +lint.flake8-quotes.inline-quotes = "double" +lint.flake8-quotes.multiline-quotes = "double" +lint.isort.known-first-party = [ + "citric", +] +lint.isort.required-imports = [ + "from __future__ import annotations", +] +lint.mccabe.max-complexity = 5 +lint.pydocstyle.convention = "google" +lint.pylint.max-args = 10 +lint.preview = true [tool.codespell] +ignore-words-list = "socio-economic" skip = ".mypy_cache,.nox,.ruff_cache,build,docs/index.md,docs/requirements.txt" +[tool.deptry] +pep621_dev_dependency_groups = [ + "dev", + "docs", +] + [tool.deptry.package_module_name_map] types-requests = "requests" typing-extensions = "typing_extensions" [tool.deptry.per_rule_ignores] +DEP001 = [ + # Notebook dependencies + "boto3", + "IPython", + "pandas", + "sqlalchemy", +] DEP002 = [ - "autodocsumm", - "colorama", "coverage", - "deptry", "faker", - "furo", - "jupyterlab", "mypy", - "myst-parser", "pytest", "pytest-github-actions-annotate-failures", "pytest-httpserver", "pytest-reverse", "pytest-subtests", "python-dotenv", - "requests-cache", "semver", - "sphinx", - "sphinx-autoapi", - "sphinx-autobuild", - "sphinx-autodoc-typehints", - "sphinx-copybutton", - "sphinx-hoverxref", - "sphinx-notfound-page", "tinydb", "xdoctest", ] DEP004 = [ - "typing_extensions", + "docutils", ] [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] addopts = [ "-vvv", "--reverse", + "-ra", # show extra test summary info for all except passed + "--strict-config", + "--strict-markers", ] filterwarnings = [ "error", "default::citric._compat.FutureVersionWarning", "always::citric._compat.CitricDeprecationWarning", ] +log_cli_level = "INFO" markers = [ "integration_test: Integration and end-to-end tests", "xfail_mysql: Mark a test as expected to fail on MySQL", ] +minversion = "8" +testpaths = [ + "tests", +] +xfail_strict = true [tool.coverage.paths] package = [ @@ -297,17 +297,31 @@ package = [ [tool.coverage.run] branch = true parallel = true -source = ["citric"] +source = [ + "citric", +] relative_files = true [tool.coverage.report] -exclude_also = ['''if (t\.)?TYPE_CHECKING:'''] +exclude_also = [ + '''if (t\.)?TYPE_CHECKING:''', +] fail_under = 85 -omit = ["src/citric/types.py"] +omit = [ + "src/citric/types.py", +] precision = 2 show_missing = true +skip_covered = true [tool.mypy] +enable_error_code = [ + "ignore-without-code", + "redundant-expr", + "truthy-bool", +] +local_partial_types = true +strict = false warn_no_return = true warn_redundant_casts = true warn_unreachable = true @@ -318,5 +332,4 @@ warn_unused_ignores = true ignore_missing_imports = true module = [ "nox.*", - "pytest_subtests.*", # TODO: Remove after https://github.com/pytest-dev/pytest-subtests/pull/115 is published ] diff --git a/requirements/requirements-lowest-direct.txt b/requirements/requirements-lowest-direct.txt new file mode 100644 index 00000000..762d3dbe --- /dev/null +++ b/requirements/requirements-lowest-direct.txt @@ -0,0 +1,12 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --universal --resolution lowest-direct -o requirements/requirements-lowest-direct.txt +certifi==2024.7.4 + # via requests +chardet==4.0.0 + # via requests +idna==2.10 + # via requests +requests==2.25.1 + # via citric (pyproject.toml) +urllib3==1.26.19 + # via requests diff --git a/src/citric/rpc/client.py b/src/citric/rpc/client.py index 0f46d067..fa59458d 100644 --- a/src/citric/rpc/client.py +++ b/src/citric/rpc/client.py @@ -1,4 +1,4 @@ -"""RPC Client.""" +"""Python API Client.""" from __future__ import annotations @@ -399,7 +399,7 @@ def _map_response_keys( >>> q1 = {"title": "Q1", "qid": 9, "gid": 7, "sid": 123} >>> q2 = {"title": "Q2", "qid": 10, "gid": 7, "sid": 123} >>> questions = {"Q1": q1, "Q2": q2} - >>> mapped_keys = RPC._map_response_keys(keys, questions) + >>> mapped_keys = Client._map_response_keys(keys, questions) >>> mapped_keys {'123X7X9': 'foo', '123X7X10': 'bar', 'BAZ': 'qux'} """ @@ -651,7 +651,7 @@ def delete_survey(self, survey_id: int) -> types.OperationStatus: """ return self.session.delete_survey(survey_id) - def export_responses( # noqa: PLR0913 + def export_responses( self, survey_id: int, *, @@ -1107,7 +1107,7 @@ def get_db_version(self) -> int: Returns: The LimeSurvey database version. - .. versionadded:: NEXT_VERSION + .. versionadded:: 1.0.0 """ return self._get_site_setting("dbversionnumber") @@ -1683,6 +1683,8 @@ def invite_participants( ) -> int: """Invite participants to a survey. + Calls :rpc_method:`invite_participants`. + Args: survey_id: ID of the survey to invite participants to. token_ids: IDs of the participants to invite. diff --git a/src/citric/rpc/session.py b/src/citric/rpc/session.py index 7670d51c..02a6ee3f 100644 --- a/src/citric/rpc/session.py +++ b/src/citric/rpc/session.py @@ -49,12 +49,15 @@ def handle_rpc_errors(result: Result, error: str | None) -> None: a non-null status. LimeSurveyApiError: The response payload has a non-null error key. """ - if isinstance(result, dict) and result.get("status") not in {"OK", None}: - raise LimeSurveyStatusError(result["status"]) - if error is not None: raise LimeSurveyApiError(error) + if not isinstance(result, dict): + return + + if result.get("status") not in {"OK", None}: + raise LimeSurveyStatusError(result["status"]) + class Session: """LimeSurvey RemoteControl 2 session. @@ -92,10 +95,6 @@ class Session: USER_AGENT = f"citric/{metadata.version('citric')}" - # TODO(edgarrmondragon): Remove this. - # https://github.com/edgarrmondragon/citric/issues/893 - _headers: t.ClassVar[dict[str, t.Any]] = {} - def __init__( self, url: str, @@ -109,7 +108,6 @@ def __init__( self.url = url self._session = requests_session or requests.session() self._session.headers["User-Agent"] = self.USER_AGENT - self._session.headers.update(self._headers) self._encoder = json_encoder or json.JSONEncoder self.__key: str | None = self.get_session_key( @@ -134,10 +132,8 @@ def __getattr__(self, name: str) -> Method[Result]: """Magic method dispatcher.""" return Method(self.rpc, name) - def rpc(self, method: str, *params: t.Any) -> Result: - """Execute RPC method on LimeSurvey, with optional token authentication. - - Any method, except for `get_session_key`. + def call(self, method: str, *params: t.Any) -> RPCResponse: + """Get the raw response from an RPC method. Args: method: Name of the method to call. @@ -152,11 +148,21 @@ def rpc(self, method: str, *params: t.Any) -> Result: # Methods requiring authentication return self._invoke(method, self.key, *params) - def _invoke( - self, - method: str, - *params: t.Any, - ) -> Result: + def rpc(self, method: str, *params: t.Any) -> Result: + """Execute a LimeSurvey RPC call with error handling. + + Args: + method: Name of the method to call. + params: Positional arguments of the RPC method. + + Returns: + An RPC result. + """ + response = self.call(method, *params) + handle_rpc_errors(response["result"], response["error"]) + return response["result"] + + def _invoke(self, method: str, *params: t.Any) -> RPCResponse: """Execute a LimeSurvey RPC with a JSON payload. Args: @@ -199,18 +205,13 @@ def _invoke( except json.JSONDecodeError as e: raise InvalidJSONResponseError from e - result = data["result"] - error = data["error"] - response_id = data["id"] logger.info("Invoked RPC method %s with ID %d", method, request_id) - handle_rpc_errors(result, error) - - if response_id != request_id: + if (response_id := data["id"]) != request_id: msg = f"Response ID {response_id} does not match request ID {request_id}" raise ResponseMismatchError(msg) - return result + return data def close(self) -> None: """Close RPC session. diff --git a/src/citric/types.py b/src/citric/types.py index ba245485..6ffb475e 100644 --- a/src/citric/types.py +++ b/src/citric/types.py @@ -1,4 +1,4 @@ -"""Citric Python types.""" +"""Citric Python types.""" # noqa: A005 from __future__ import annotations diff --git a/tests/conftest.py b/tests/conftest.py index 08c7ad39..867e7ee4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import json import os import typing as t +from importlib.metadata import version import pytest import requests @@ -66,6 +67,13 @@ def pytest_addoption(parser: pytest.Parser): default=_from_env_var("LS_PASSWORD"), ) + parser.addoption( + "--mailhog-url", + action="store", + help="URL of the MailHog instance to test against.", + default=_from_env_var("MAILHOG_URL", "http://localhost:8025"), + ) + def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]): """Modify test collection.""" @@ -91,6 +99,22 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item _add_integration_skip(item, value, reason) +def pytest_report_header() -> list[str]: + """Return a list of strings to be displayed in the header of the report.""" + env_vars = [ + f"{key}: {value}" + for key, value in os.environ.items() + if key.startswith(("COVERAGE_", "NOX_")) + ] + + dependencies = [ + f"requests: {version('requests')}", + f"urllib3: {version('urllib3')}", + ] + + return env_vars + dependencies + + @pytest.fixture(scope="session") def integration_url(request: pytest.FixtureRequest) -> str: """LimeSurvey URL.""" @@ -109,6 +133,12 @@ def integration_password(request: pytest.FixtureRequest) -> str: return request.config.getoption("--limesurvey-password") +@pytest.fixture(scope="session") +def integration_mailhog_url(request: pytest.FixtureRequest) -> str: + """MailHog URL.""" + return request.config.getoption("--mailhog-url") + + class LimeSurveyMockAdapter(BaseAdapter): """Requests adapter that mocks LSRC2 API calls.""" diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 00000000..91a0faf3 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,20 @@ +"""MailHog API client.""" + +from __future__ import annotations + +import requests + + +class MailHogClient: + """MailHog API client.""" + + def __init__(self, base_url: str) -> None: + self.base_url = base_url + + def get_all(self) -> dict: + """Get all messages.""" + return requests.get(f"{self.base_url}/api/v2/messages", timeout=10).json() + + def delete(self) -> None: + """Delete all messages.""" + requests.delete(f"{self.base_url}/api/v1/messages", timeout=10) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 95b3e9d5..3ad91721 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -11,6 +11,7 @@ import citric from citric.exceptions import LimeSurveyStatusError +from tests.fixtures import MailHogClient @pytest.fixture(scope="session") @@ -53,3 +54,11 @@ def server_version(client: citric.Client) -> semver.Version: def database_version(client: citric.Client) -> int: """Get the LimeSurvey database schema version.""" return client.get_db_version() + + +@pytest.fixture +def mailhog(integration_mailhog_url: str) -> MailHogClient: + """Get the LimeSurvey database schema version.""" + client = MailHogClient(integration_mailhog_url) + client.delete() + return client diff --git a/tests/integration/test_rest_client.py b/tests/integration/test_rest_client.py index b9aff37d..775e553f 100644 --- a/tests/integration/test_rest_client.py +++ b/tests/integration/test_rest_client.py @@ -5,12 +5,10 @@ import typing as t import pytest +import semver from citric.rest import RESTClient -if t.TYPE_CHECKING: - import semver - @pytest.fixture(scope="module") def rest_client( @@ -63,12 +61,16 @@ def test_patch_survey_details( tokenLength=token_length + 10, ) expected = ( - { + True + if server_version < semver.Version(6, 4, prerelease="dev") + else { "operationsApplied": 1, "erronousOperations": [], } - if server_version >= (6, 4) - else True + if server_version < semver.Version(6, 5, prerelease="dev") + else { + "operationsApplied": 1, + } ) assert result == expected diff --git a/tests/integration/test_rpc_client.py b/tests/integration/test_rpc_client.py index a5dedeaa..17d309d2 100644 --- a/tests/integration/test_rpc_client.py +++ b/tests/integration/test_rpc_client.py @@ -5,6 +5,8 @@ import csv import io import json +import operator +import random import typing as t import uuid from datetime import datetime @@ -24,6 +26,8 @@ from faker import Faker from pytest_subtests import SubTests + from tests.fixtures import MailHogClient + NEW_SURVEY_NAME = "New Survey" @@ -157,6 +161,25 @@ def test_survey(client: citric.Client): assert new_props["format"] == enums.NewSurveyType.ALL_ON_ONE_PAGE +@pytest.mark.integration_test +def test_import_survey(client: citric.Client, subtests: SubTests): + """Test importing a survey with a custom ID and name.""" + survey_id = random.randint(10000, 20000) # noqa: S311 + with Path("./examples/survey.lss").open("rb") as f: + imported_id = client.import_survey( + f, + survey_id=survey_id, + survey_name="Custom Name", + ) + + with subtests.test(msg="imported survey has custom ID"): + assert imported_id == survey_id + + survey_props = client.get_language_properties(imported_id) + with subtests.test(msg="imported survey has custom name"): + assert survey_props["surveyls_title"] == "Custom Name" + + @pytest.mark.integration_test def test_copy_survey_destination_id( request: pytest.FixtureRequest, @@ -205,7 +228,7 @@ def test_group(client: citric.Client, survey_id: int): questions = sorted( client.list_questions(survey_id, group_id), - key=lambda q: q["qid"], + key=operator.itemgetter("qid"), ) assert questions[0]["question"] == "

First question

" @@ -752,3 +775,85 @@ def test_users(client: citric.Client): def test_survey_groups(client: citric.Client): """Test survey group methods.""" assert len(client.list_survey_groups()) == 1 + + +@pytest.mark.integration_test +def test_mail_registered_participants( + client: citric.Client, + survey_id: int, + participants: list[dict[str, str]], + mailhog: MailHogClient, + subtests: SubTests, +): + """Test mail_registered_participants.""" + client.activate_survey(survey_id) + client.activate_tokens(survey_id, [1, 2]) + client.add_participants( + survey_id, + participant_data=participants, + create_tokens=False, + ) + + with subtests.test(msg="No initial emails"): + assert mailhog.get_all()["total"] == 0 + + # `mail_registered_participants` returns a non-error status messages even when + # emails are sent successfully and that violates assumptions made by this + # library about the meaning of `status` messages + with pytest.raises( + LimeSurveyStatusError, + match="0 left to send", + ): + client.session.mail_registered_participants(survey_id) + + with subtests.test(msg="2 emails sent"): + assert mailhog.get_all()["total"] == 2 + + mailhog.delete() + + with pytest.raises( + LimeSurveyStatusError, + match="Error: No candidate tokens", + ): + client.session.mail_registered_participants(survey_id) + + with subtests.test(msg="No more emails sent"): + assert mailhog.get_all()["total"] == 0 + + +@pytest.mark.integration_test +def test_remind_participants( + client: citric.Client, + survey_id: int, + participants: list[dict[str, str]], + mailhog: MailHogClient, + subtests: SubTests, +): + """Test remind_participants.""" + client.activate_survey(survey_id) + client.activate_tokens(survey_id, [1, 2]) + client.add_participants( + survey_id, + participant_data=participants, + create_tokens=False, + ) + + with subtests.test(msg="No initial emails"): + assert mailhog.get_all()["total"] == 0 + + # Use `call` to avoid error handling + client.session.call("mail_registered_participants", survey_id) + + with subtests.test(msg="2 emails sent"): + assert mailhog.get_all()["total"] == 2 + + mailhog.delete() + + # `remind_participants` returns a non-error status messages even when emails are + # sent successfully and that violates assumptions made by this library about the + # meaning of `status` messages" + with pytest.raises(LimeSurveyStatusError, match="0 left to send"): + client.session.remind_participants(survey_id) + + with subtests.test(msg="2 reminders sent"): + assert mailhog.get_all()["total"] == 2