Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into login-method
Browse files Browse the repository at this point in the history
  • Loading branch information
leplatrem committed Nov 12, 2024
2 parents 7c12eee + 4ec468c commit 825579d
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 10 deletions.
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,22 @@ The history of a bucket can also be purged with:
client.purge_history(bucket='default')
Attachments
-----------

If the `kinto-attachment plugin <https://github.com/Kinto/kinto-attachment/>`_ is enabled, it is possible to add attachments on records:

.. code-block:: python
client.add_attachment(id="record-id", filepath="/path/to/image.png")
Or remove them:

.. code-block:: python
client.remove_attachment(id="record-id")
Endpoint URLs
-------------

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ build-backend = "setuptools.build_meta"
[project.optional-dependencies]
dev = [
"kinto",
"kinto-attachment",
"ruff",
"pytest",
"pytest-asyncio",
Expand Down
36 changes: 36 additions & 0 deletions src/kinto_http/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import asyncio
import functools
import inspect
import json
import logging
import mimetypes
import os
import uuid
from collections import OrderedDict
from contextlib import contextmanager
Expand Down Expand Up @@ -865,6 +868,39 @@ def purge_history(self, *, bucket=None, safe=True, if_match=None) -> List[Dict]:
resp, _ = self.session.request("delete", endpoint, headers=headers)
return resp["data"]

@retry_timeout
def add_attachment(
self,
id,
filepath,
bucket=None,
collection=None,
data=None,
permissions=None,
mimetype=None,
):
with open(filepath, "rb") as f:
filecontent = f.read()
filename = os.path.basename(filepath)
if mimetype is None:
mimetype, _ = mimetypes.guess_type(filepath)
multipart = [("attachment", (filename, filecontent, mimetype))]
endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection)
resp, _ = self.session.request(
"post",
endpoint,
data=json.dumps(data) if data is not None else None,
permissions=json.dumps(permissions) if permissions is not None else None,
files=multipart,
)
return resp

@retry_timeout
def remove_attachment(self, id, bucket=None, collection=None):
endpoint = self._get_endpoint("attachment", id=id, bucket=bucket, collection=collection)
resp, _ = self.session.request("delete", endpoint)
return resp

def __repr__(self) -> str:
if self.collection_name:
endpoint = self._get_endpoint(
Expand Down
1 change: 1 addition & 0 deletions src/kinto_http/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Endpoints(object):
"collection": "{root}/buckets/{bucket}/collections/{collection}",
"records": "{root}/buckets/{bucket}/collections/{collection}/records", # NOQA
"record": "{root}/buckets/{bucket}/collections/{collection}/records/{id}", # NOQA
"attachment": "{root}/buckets/{bucket}/collections/{collection}/records/{id}/attachment", # NOQA
}

def __init__(self, root=""):
Expand Down
3 changes: 3 additions & 0 deletions tests/config/kinto.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ kinto.paginate_by = 5

kinto.includes = kinto.plugins.flush
kinto.plugins.accounts
kinto_attachment

multiauth.policies = account
multiauth.policy.account.use = kinto.plugins.accounts.AccountsPolicy
kinto.account_create_principals = system.Everyone
kinto.account_write_principals = account:user
kinto.bucket_create_principals = account:user

kinto.attachment.base_path = /tmp

[server:main]
use = egg:waitress#main
host = 0.0.0.0
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

import pytest
import requests
from pytest_mock.plugin import MockerFixture

from kinto_http import AsyncClient, Client
from kinto_http.constants import DEFAULT_AUTH, SERVER_URL, USER_AGENT
from kinto_http.endpoints import Endpoints
from kinto_http.exceptions import KintoException
from kinto_http.session import Session
from pytest_mock.plugin import MockerFixture

from .support import create_user, get_200, get_503, mock_response

Expand Down
1 change: 1 addition & 0 deletions tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from urllib.parse import urljoin

import requests

from kinto_http.constants import DEFAULT_AUTH
from kinto_http.exceptions import KintoException

Expand Down
3 changes: 2 additions & 1 deletion tests/test_async_client.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import re

import pytest
from pytest_mock import MockerFixture

from kinto_http import AsyncClient as Client
from kinto_http import BearerTokenAuth, BucketNotFound, KintoException
from kinto_http.constants import DO_NOT_OVERWRITE, SERVER_URL
from kinto_http.patch_type import JSONPatch, MergePatch
from kinto_http.session import create_session
from pytest_mock import MockerFixture

from .support import build_response, get_http_error, mock_response

Expand Down
3 changes: 2 additions & 1 deletion tests/test_batch.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pytest
from pytest_mock.plugin import MockerFixture

from kinto_http import Client
from kinto_http.batch import BatchSession
from kinto_http.exceptions import KintoException
from pytest_mock.plugin import MockerFixture


def test_requests_are_stacked(batch_setup: Client, mocker: MockerFixture):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import argparse

from pytest_mock import MockerFixture

from kinto_http import BearerTokenAuth, cli_utils
from kinto_http.constants import ALL_PARAMETERS
from pytest_mock import MockerFixture

from .support import assert_option_strings

Expand Down
25 changes: 24 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re

import pytest
from pytest_mock.plugin import MockerFixture

from kinto_http import (
BearerTokenAuth,
BucketNotFound,
Expand All @@ -11,7 +13,6 @@
)
from kinto_http.constants import DO_NOT_OVERWRITE, SERVER_URL
from kinto_http.patch_type import JSONPatch, MergePatch
from pytest_mock.plugin import MockerFixture

from .support import build_response, get_http_error, mock_response

Expand Down Expand Up @@ -1391,3 +1392,25 @@ def test_purging_of_history(client_setup: Client):
client.purge_history(bucket="mybucket")
url = "/buckets/mybucket/history"
client.session.request.assert_called_with("delete", url, headers=None)


def test_add_attachment_guesses_mimetype(record_setup: Client, tmp_path):
client = record_setup
mock_response(client.session)

p = tmp_path / "file.txt"
p.write_text("hello")
client.add_attachment(
id="abc",
bucket="a",
collection="b",
filepath=p,
)

client.session.request.assert_called_with(
"post",
"/buckets/a/collections/b/records/abc/attachment",
data=None,
permissions=None,
files=[("attachment", ("file.txt", b"hello", "text/plain"))],
)
1 change: 1 addition & 0 deletions tests/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Dict, Tuple

import pytest

from kinto_http import KintoException
from kinto_http.endpoints import Endpoints

Expand Down
46 changes: 46 additions & 0 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from unittest import mock

import pytest

from kinto_http import BucketNotFound, CollectionNotFound, KintoException, replication
from kinto_http.patch_type import JSONPatch

Expand Down Expand Up @@ -536,3 +537,48 @@ def test_replication(functional_setup):
replication.replicate(origin, destination)
records = client.get_records(bucket="destination", collection="coll")
assert len(records) == 10


def test_adding_an_attachment(functional_setup, tmp_path):
client = functional_setup
with client.batch(bucket="mozilla", collection="payments") as batch:
batch.create_bucket()
batch.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")

client.add_attachment(
id="abc",
filepath=p,
bucket="mozilla",
collection="payments",
data={"secret": "psssssst!"},
permissions={"write": ["system.Everyone"]},
mimetype="text/custom",
)

record = client.get_record(bucket="mozilla", collection="payments", id="abc")
assert "attachment" in record["data"]
assert record["data"]["attachment"]["filename"] == "file.txt"
assert record["data"]["attachment"]["mimetype"] == "text/custom"
assert "secret" in record["data"]
assert "system.Everyone" in record["permissions"]["write"]


def test_removing_an_attachment(functional_setup, tmp_path):
client = functional_setup.clone(bucket="mozilla", collection="payments")
with client.batch() as batch:
batch.create_bucket()
batch.create_collection()
p = tmp_path / "file.txt"
p.write_text("hello")
client.add_attachment(
id="abc",
filepath=p,
)

client.remove_attachment(id="abc")

record = client.get_record(id="abc")
assert record["data"]["attachment"] is None
45 changes: 44 additions & 1 deletion tests/test_functional_async.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import pytest
from pytest_mock import MockerFixture

from kinto_http import BucketNotFound, CollectionNotFound, KintoException
from kinto_http.patch_type import JSONPatch
from pytest_mock import MockerFixture

from .support import get_user_id

Expand Down Expand Up @@ -518,3 +519,45 @@ async def test_patch_record_jsonpatch(functional_async_setup):
assert record["data"]["hello"] == "world"
assert record["data"]["goodnight"] == "moon"
assert record["permissions"]["read"] == ["alice"]


async def test_adding_an_attachment(functional_async_setup, tmp_path):
client = functional_async_setup.clone(bucket="mozilla", collection="payments")
await client.create_bucket()
await client.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")

await client.add_attachment(
id="abc",
filepath=p,
data={"secret": "psssssst!"},
permissions={"write": ["system.Everyone"]},
mimetype="text/custom",
)

record = await client.get_record(id="abc")
assert "attachment" in record["data"]
assert record["data"]["attachment"]["filename"] == "file.txt"
assert record["data"]["attachment"]["mimetype"] == "text/custom"
assert "secret" in record["data"]
assert "system.Everyone" in record["permissions"]["write"]


async def test_removing_an_attachment(functional_async_setup, tmp_path):
client = functional_async_setup.clone(bucket="mozilla", collection="payments")
await client.create_bucket()
await client.create_collection()

p = tmp_path / "file.txt"
p.write_text("hello")
await client.add_attachment(
id="abc",
filepath=p,
)

await client.remove_attachment(id="abc")

record = await client.get_record(id="abc")
assert record["data"]["attachment"] is None
3 changes: 2 additions & 1 deletion tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from kinto_http import Client
from pytest_mock.plugin import MockerFixture

from kinto_http import Client


def test_create_bucket_logs_info_message(client_setup: Client, mocker: MockerFixture):
mocked_logger = mocker.patch("kinto_http.client.logger")
Expand Down
3 changes: 2 additions & 1 deletion tests/test_replication.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from pytest_mock import MockerFixture

from kinto_http import Client, exceptions
from kinto_http.replication import replicate
from pytest_mock import MockerFixture

from .support import mock_response

Expand Down
5 changes: 3 additions & 2 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from typing import Tuple
from unittest.mock import MagicMock

import kinto_http
import pkg_resources
import pytest
from pytest_mock.plugin import MockerFixture

import kinto_http
from kinto_http.constants import USER_AGENT
from kinto_http.exceptions import BackoffException, KintoException
from kinto_http.session import Session, create_session
from pytest_mock.plugin import MockerFixture

from .support import get_200, get_403, get_503, get_http_response

Expand Down

0 comments on commit 825579d

Please sign in to comment.