From c1b9cc26d3829911a703898011823e2ff74f762f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Rami=CC=81rez=20Mondrago=CC=81n?= Date: Sun, 1 May 2022 22:47:06 -0500 Subject: [PATCH] feat: add `get_uploaded_file_objects` to Client --- README.md | 4 -- docs/examples.md | 12 ++---- noxfile.py | 10 ++++- src/citric/client.py | 96 +++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 862385fe..cee7e3a8 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,6 @@ Code samples and API documentation are available at [citric.readthedocs.io](http If you'd like to contribute to this project, please see the [contributing guide](https://citric.readthedocs.io/en/latest/contributing/getting-started.html). -## Contributing - -If you'd like to contribute to this project, please see the [contributing guide](https://citric.readthedocs.io/en/latest/contributing/getting-started.html). - ## Credits - [Claudio Jolowicz][claudio] and [his amazing blog post][hypermodern]. diff --git a/docs/examples.md b/docs/examples.md index 7145a4ad..ac119f00 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -88,9 +88,6 @@ Common plugins are `Authdb` (default), `AuthLDAP` and `Authwebserver`. ## Get uploaded files and move them to S3 ```python -import base64 -import io - import boto3 from citric import Client @@ -102,14 +99,11 @@ with Client( "secret", ) as client: survey_id = 12345 - files = client.get_uploaded_files(survey_id) - for file in files: - content = base64.b64decode(files[file]["content"]) # Decode content - question_id = files[file]["meta"]["question"]["qid"] + for file in client.get_uploaded_file_objects(survey_id): s3.upload_fileobj( - io.BytesIO(content), + file.content, "my-s3-bucket", - f"uploads/{survey_id}/{question_id}/{file}", + f"uploads/sid={survey_id}/qid={file.meta.question.qid}/{file.meta.filename}", ) ``` diff --git a/noxfile.py b/noxfile.py index 824ea2c2..3a3d7d75 100644 --- a/noxfile.py +++ b/noxfile.py @@ -180,7 +180,15 @@ def docs_build(session: Session) -> None: @session(name="docs-serve", python=main_python_version) def docs_serve(session: Session) -> None: """Build the documentation.""" - args = session.posargs or ["--open-browser", "--watch", ".", "docs", "build"] + args = session.posargs or [ + "--open-browser", + "--watch", + ".", + "--ignore", + "**/.nox/*", + "docs", + "build", + ] session.install(".[docs]") build_dir = Path("build") diff --git a/src/citric/client.py b/src/citric/client.py index a839f97e..87faff31 100644 --- a/src/citric/client.py +++ b/src/citric/client.py @@ -4,11 +4,13 @@ import base64 import datetime +import io import sys +from dataclasses import dataclass from os import PathLike from pathlib import Path from types import TracebackType -from typing import Any, BinaryIO, Iterable, Mapping, Sequence, TypeVar +from typing import Any, BinaryIO, Generator, Iterable, Mapping, Sequence, TypeVar import requests @@ -24,6 +26,61 @@ _T = TypeVar("_T", bound="Client") +@dataclass +class QuestionReference: + """Uploaded file question reference.""" + + title: str + """Question title.""" + + qid: int + """Question ID.""" + + +@dataclass +class FileMetadata: + """Uploaded file metadata.""" + + title: str + """File title.""" + + comment: str + """File comment.""" + + name: str + """File name.""" + + filename: str + """LimeSurvey internal file name.""" + + size: float + """File size in bytes.""" + + ext: str + """File extension.""" + + question: QuestionReference + """:class:`~citric.client.QuestionReference` object.""" + + index: int + """File index.""" + + +@dataclass +class UploadedFile: + """A file uploaded to a survey response.""" + + meta: FileMetadata + """:class:`~citric.client.FileMetadata` object.""" + + content: io.BytesIO + """File content as `io.BytesIO`_. + + .. _io.BytesIO: + https://docs.python.org/3/library/io.html#io.BytesIO + """ + + class Client: """LimeSurvey Remote Control client. @@ -715,6 +772,34 @@ def get_uploaded_files( """ return self.__session.get_uploaded_files(survey_id, token) + def get_uploaded_file_objects( + self, + survey_id: int, + token: str | None = None, + ) -> Generator[UploadedFile, None, None]: + """Iterate over uploaded files in a survey response. + + Args: + survey_id: Survey for which to download files. + token: Optional participant token to filter uploaded files. + + Yields: + :class:`~citric.client.UploadedFile` objects. + """ + files_data = self.get_uploaded_files(survey_id, token) + for file in files_data: + metadata: dict = files_data[file]["meta"] + question: dict = metadata.pop("question") + content = base64.b64decode(files_data[file]["content"]) + + yield UploadedFile( + meta=FileMetadata( + **metadata, + question=QuestionReference(**question), + ), + content=io.BytesIO(content), + ) + def download_files( self, directory: str | Path, @@ -734,14 +819,13 @@ def download_files( dirpath = Path(directory) filepaths = [] - files_data = self.get_uploaded_files(survey_id, token=token) + uploaded_files = self.get_uploaded_file_objects(survey_id, token=token) - for file in files_data: - metadata = files_data[file]["meta"] - filepath = dirpath / metadata["filename"] + for file in uploaded_files: + filepath = dirpath / file.meta.filename filepaths.append(filepath) with open(filepath, "wb") as f: - f.write(base64.b64decode(files_data[file]["content"])) + f.write(file.content.read()) return filepaths