From 8bbca6421fea5ba8d47e57bb5bb4690074428aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Ram=C3=ADrez=20Mondrag=C3=B3n?= <16805946+edgarrmondragon@users.noreply.github.com> Date: Wed, 15 Nov 2023 20:24:36 -0600 Subject: [PATCH] docs: Link to how-to doc from index page (#1032) * docs: Link to how-to doc from index page Closes https://github.com/edgarrmondragon/citric/issues/1030: * Fix samples * Fix header * Update ruff.toml --- README.md | 26 ++--- code_samples/auth_plugin.py | 14 +++ code_samples/context_manager.py | 13 +++ code_samples/custom_requests_session.py | 24 +++++ code_samples/duckdb_sql.py | 31 ++++++ code_samples/get_surveys.py | 24 +++++ code_samples/pandas_df.py | 27 +++++ code_samples/ruff.toml | 8 ++ code_samples/session_attr.py | 16 +++ code_samples/upload_s3.py | 26 +++++ docs/conf.py | 2 + docs/how-to.md | 136 +++++------------------- docs/index.md | 30 ++++-- pyproject.toml | 10 +- 14 files changed, 252 insertions(+), 135 deletions(-) create mode 100644 code_samples/auth_plugin.py create mode 100644 code_samples/context_manager.py create mode 100644 code_samples/custom_requests_session.py create mode 100644 code_samples/duckdb_sql.py create mode 100644 code_samples/get_surveys.py create mode 100644 code_samples/pandas_df.py create mode 100644 code_samples/ruff.toml create mode 100644 code_samples/session_attr.py create mode 100644 code_samples/upload_s3.py diff --git a/README.md b/README.md index 136dab1d..34606c4a 100644 --- a/README.md +++ b/README.md @@ -44,10 +44,12 @@ -A client to the [LimeSurvey Remote Control API 2](https://manual.limesurvey.org/RemoteControl_2_API), written in modern -Python. +*A client to the [LimeSurvey Remote Control API 2](https://manual.limesurvey.org/RemoteControl_2_API), written in modern Python.* + + + ## Features - Supports the full RPC API via the [`Session` class](https://citric.readthedocs.io/en/latest/_api/citric/session/index.html#citric.session.Session). @@ -58,16 +60,14 @@ Python. ### Integration tests - -| | **PostgreSQL** | **MySQL** | -| - |:--: | :-: | -| 6.3.4 | ✅ | ✅ | -| 6.3.3 | ✅ | ✅ | -| 6.3.1 | ✅ | ✅ | -| 5.6.44 | ✅ | ✅ | -| 5.6.43 | ✅ | ✅ | -| 5.6.42 | ✅ | ✅ | - +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.3.4](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.3.4%2B231108) +- [6.3.3](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.3.3%2B231106) +- [6.3.1](https://github.com/LimeSurvey/LimeSurvey/releases/tag/6.3.1%2B231023) +- [5.6.44](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.44%2B231107) +- [5.6.43](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.43%2B231030) +- [5.6.42](https://github.com/LimeSurvey/LimeSurvey/releases/tag/5.6.42%2B231024) ## Installation @@ -95,6 +95,8 @@ for survey in client.list_surveys(): print(survey["surveyls_title"]) ``` + + ## Documentation Code samples and API documentation are available at [citric.readthedocs.io](https://citric.readthedocs.io/). diff --git a/code_samples/auth_plugin.py b/code_samples/auth_plugin.py new file mode 100644 index 00000000..3fc4d592 --- /dev/null +++ b/code_samples/auth_plugin.py @@ -0,0 +1,14 @@ +"""Example of using an auth plugin.""" + +from __future__ import annotations + +# start example +from citric import Client + +client = Client( + "https://example.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", + auth_plugin="AuthLDAP", +) +# end example diff --git a/code_samples/context_manager.py b/code_samples/context_manager.py new file mode 100644 index 00000000..feae6908 --- /dev/null +++ b/code_samples/context_manager.py @@ -0,0 +1,13 @@ +"""Example of using the context manager to automatically clean up the session key.""" + +from __future__ import annotations + +# start example +from citric import Client + +LS_URL = "http://localhost:8001/index.php/admin/remotecontrol" + +with Client(LS_URL, "iamadmin", "secret") as client: + # Do stuff with the client + ... +# end example diff --git a/code_samples/custom_requests_session.py b/code_samples/custom_requests_session.py new file mode 100644 index 00000000..1f621426 --- /dev/null +++ b/code_samples/custom_requests_session.py @@ -0,0 +1,24 @@ +"""Example of using a custom requests.Session object with the client.""" + +from __future__ import annotations + +# start example +import requests_cache +from citric import Client + +cached_session = requests_cache.CachedSession( + expire_after=60, + allowable_methods=["POST"], +) + +client = Client( + "https://example.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", + requests_session=cached_session, +) + +# Get all surveys from user "iamadmin". +# All responses will be cached for 1 minute. +surveys = client.list_surveys("iamadmin") +# end example diff --git a/code_samples/duckdb_sql.py b/code_samples/duckdb_sql.py new file mode 100644 index 00000000..b93d07d5 --- /dev/null +++ b/code_samples/duckdb_sql.py @@ -0,0 +1,31 @@ +"""Export survey responses to a DuckDB database.""" + +from __future__ import annotations + +# ruff: noqa: I001, PTH123 + +# start example +import citric +import duckdb + +client = citric.Client( + "https://mylimeserver.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", +) + +with open("responses.csv", "wb") as file: + file.write(client.export_responses(12345, file_format="csv")) + +duckdb.execute("CREATE TABLE responses AS SELECT * FROM 'responses.csv'") +duckdb.sql( + """ + SELECT + token, + submitdate - startdate AS duration + FROM responses + ORDER BY 2 DESC + LIMIT 10 +""" +).show() +# end example diff --git a/code_samples/get_surveys.py b/code_samples/get_surveys.py new file mode 100644 index 00000000..1d6d8b7a --- /dev/null +++ b/code_samples/get_surveys.py @@ -0,0 +1,24 @@ +"""Get all surveys and questions from user "iamadmin".""" + +# ruff: noqa: T201 + +from __future__ import annotations + +# start example +from citric import Client + +LS_URL = "http://localhost:8001/index.php/admin/remotecontrol" + +client = Client(LS_URL, "iamadmin", "secret") + +# Get all surveys from user "iamadmin" +surveys = client.list_surveys("iamadmin") + +for s in surveys: + print(s["surveyls_title"]) + + # Get all questions, regardless of group + questions = client.list_questions(s["sid"]) + for q in questions: + print(q["title"], q["question"]) +# end example diff --git a/code_samples/pandas_df.py b/code_samples/pandas_df.py new file mode 100644 index 00000000..45fe4956 --- /dev/null +++ b/code_samples/pandas_df.py @@ -0,0 +1,27 @@ +"""Export survey responses to a Pandas DataFrame.""" + +from __future__ import annotations + +# ruff: noqa: PD901, S106 +# start example +import io + +import pandas as pd +from citric import Client + +survey_id = 123456 + +client = Client( + "https://mylimeserver.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", +) + +# Export responses to CSV and read into a Pandas DataFrame +df = pd.read_csv( + io.BytesIO(client.export_responses(survey_id, file_format="csv")), + delimiter=";", + parse_dates=["datestamp", "startdate", "submitdate"], + index_col="id", +) +# end example diff --git a/code_samples/ruff.toml b/code_samples/ruff.toml new file mode 100644 index 00000000..8eaab689 --- /dev/null +++ b/code_samples/ruff.toml @@ -0,0 +1,8 @@ +extend = "../pyproject.toml" +ignore = [ + "INP001", # implicit-namespace-package +] + +[lint.isort] +known-first-party = [] +known-third-party = ["citric"] diff --git a/code_samples/session_attr.py b/code_samples/session_attr.py new file mode 100644 index 00000000..2e3c7946 --- /dev/null +++ b/code_samples/session_attr.py @@ -0,0 +1,16 @@ +"""Example of using the Client.session attribute to call RPC methods directly.""" + +from __future__ import annotations + +from citric import Client + +# start example +client = Client( + "https://mylimeserver.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", +) + +# Call the copy_survey method, not available in Client +new_survey_id = client.session.copy_survey(35239, "copied_survey") +# end example diff --git a/code_samples/upload_s3.py b/code_samples/upload_s3.py new file mode 100644 index 00000000..13f8018e --- /dev/null +++ b/code_samples/upload_s3.py @@ -0,0 +1,26 @@ +"""Upload survey files to S3.""" + +from __future__ import annotations + +# start example +import boto3 +from citric import Client + +s3 = boto3.client("s3") + +client = Client( + "https://mylimeserver.com/index.php/admin/remotecontrol", + "iamadmin", + "secret", +) + +survey_id = 12345 + +# Get all uploaded files and upload them to S3 +for file in client.get_uploaded_file_objects(survey_id): + s3.upload_fileobj( + file.content, + "my-s3-bucket", + f"uploads/sid={survey_id}/qid={file.meta.question.qid}/{file.meta.filename}", + ) +# end example diff --git a/docs/conf.py b/docs/conf.py index 2cddec17..a8f5ef20 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,6 +29,8 @@ "notfound.extension", ] +myst_heading_anchors = 2 + autodoc_typehints = "description" autodoc_typehints_description_target = "documented" diff --git a/docs/how-to.md b/docs/how-to.md index 39b71f57..b319b72d 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -4,79 +4,32 @@ For the full JSON-RPC reference, see the [RemoteControl 2 API docs][rc2api]. ## Automatically close the session with a context manager -```python -from citric import Client - -LS_URL = "http://localhost:8001/index.php/admin/remotecontrol" - -with Client(LS_URL, "iamadmin", "secret") as client: - # Do stuff with the client - ... +```{literalinclude} ../code_samples/context_manager.py +:start-after: start example +:end-before: end example ``` Otherwise, you can manually close the session with {meth}`client.close() `. ## Get surveys and questions -```python -from citric import Client - -LS_URL = "http://localhost:8001/index.php/admin/remotecontrol" - -client = Client(LS_URL, "iamadmin", "secret") - -# Get all surveys from user "iamadmin" -surveys = client.list_surveys("iamadmin") - -for s in surveys: - print(s["surveyls_title"]) - - # Get all questions, regardless of group - questions = client.list_questions(s["sid"]) - for q in questions: - print(q["title"], q["question"]) +```{literalinclude} ../code_samples/get_surveys.py +:start-after: start example +:end-before: end example ``` ## Export responses to a `pandas` dataframe -```python -import io -import pandas as pd -from citric import Client - -survey_id = 123456 - -client = citric.Client(...) - -# Export responses to CSV and read into a Pandas DataFrame -df = pd.read_csv( - io.BytesIO(client.export_responses(survey_id, file_format="csv")), - delimiter=";", - parse_dates=["datestamp", "startdate", "submitdate"], - index_col="id", -) +```{literalinclude} ../code_samples/pandas_df.py +:start-after: start example +:end-before: end example ``` ## Export responses to a [DuckDB](https://duckdb.org/) database and analyze with SQL -```python -import citric -import duckdb - -client = citric.Client(...) - -with open("responses.csv", "wb") as file: - file.write(client.export_responses(, file_format="csv")) - -duckdb.execute("CREATE TABLE responses AS SELECT * FROM 'responses.csv'") -duckdb.sql(""" - SELECT - token, - submitdate - startdate AS duration - FROM responses - ORDER BY 2 DESC - LIMIT 10 -""").show() +```{literalinclude} ../code_samples/duckdb_sql.py +:start-after: start example +:end-before: end example ``` ## Use custom `requests` session @@ -85,24 +38,9 @@ It's possible to use a custom session object to make requests. For example, to c and reduce the load on your server in read-intensive applications, you can use [`requests-cache`](inv:requests-cache:std#general): -```python -import requests_cache - -cached_session = requests_cache.CachedSession( - expire_after=60, - allowable_methods=["POST"], -) - -client = Client( - LS_URL, - "iamadmin", - "secret", - requests_session=cached_session, -) - -# Get all surveys from user "iamadmin". -# All responses will be cached for 1 minute. -surveys = client.list_surveys("iamadmin") +```{literalinclude} ../code_samples/custom_requests_session.py +:start-after: start example +:end-before: end example ``` ## Use a different authentication plugin @@ -111,51 +49,27 @@ By default, this client uses the internal database for authentication but {ls_manual}`arbitrary plugins ` are supported by the `auth_plugin` argument. -```python -client = Client( - LS_URL, - "iamadmin", - "secret", - auth_plugin="AuthLDAP", -) +```{literalinclude} ../code_samples/auth_plugin.py +:start-after: start example +:end-before: end example ``` Common plugins are `Authdb` (default), `AuthLDAP` and `Authwebserver`. ## Get files uploaded to a survey and move them to S3 -```python -import boto3 -from citric import Client - -s3 = boto3.client("s3") - -client = Client( - "https://mylimeserver.com/index.php/admin/remotecontrol", - "iamadmin", - "secret", -) - -survey_id = 12345 - -# Get all uploaded files and upload them to S3 -for file in client.get_uploaded_file_objects(survey_id): - s3.upload_fileobj( - file.content, - "my-s3-bucket", - f"uploads/sid={survey_id}/qid={file.meta.question.qid}/{file.meta.filename}", - ) +```{literalinclude} ../code_samples/upload_s3.py +:start-after: start example +:end-before: end example ``` -## Use the raw `Client.session` for low-level interaction +## 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: -```python -client = Client(LS_URL, "iamadmin", "secret") - -# Call the copy_survey method, not available in Client -new_survey_id = client.session.copy_survey(35239, "copied_survey") +```{literalinclude} ../code_samples/session_attr.py +:start-after: start example +:end-before: end example ``` ## Notebook samples diff --git a/docs/index.md b/docs/index.md index 9bca303e..fbcb9a7b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,18 +8,27 @@ # Citric -Release v{sub-ref}`version`. +*A client to the [LimeSurvey Remote Control API 2](https://manual.limesurvey.org/RemoteControl_2_API), written in modern +Python.* + +Release **v{sub-ref}`version`**. ([What's new?](./changelog.md)) ```{include} ../README.md -:start-line: 4 +:start-after: +:end-before: ``` -```{toctree} -:maxdepth: 1 -:hidden: +## How-to guides -license -``` +- [Automatically close the session with a context manager](how-to.md#automatically-close-the-session-with-a-context-manager) +- [Get surveys and questions](how-to.md#get-surveys-and-questions) +- [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) +- [Use custom `requests` session](how-to.md#use-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 `Client.session` for low-level interaction](how-to.md#use-the-session-attribute-for-low-level-interaction) +- [Notebook samples](how-to.md#notebook-samples) ```{toctree} :maxdepth: 2 @@ -48,6 +57,13 @@ RPC method coverage Python API reference <_api/index> ``` +```{toctree} +:maxdepth: 1 +:hidden: + +license +``` + ```{toctree} :caption: Contributing :hidden: diff --git a/pyproject.toml b/pyproject.toml index 6c35b90f..016225a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -150,7 +150,11 @@ unfixable = [ "ERA", # Don't remove commented out code ] -[tool.ruff.per-file-ignores] +[tool.ruff.format] +# Enable preview style formatting. +preview = true + +[tool.ruff.lint.per-file-ignores] "docs/notebooks/*" = [ "D100", # undocumented-public-module "INP001", # implicit-namespace-package @@ -172,10 +176,6 @@ unfixable = [ "PLR6301", # no-self-use ] -[tool.ruff.format] -# Enable preview style formatting. -preview = true - [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "double"