Passed
Pull Request — main (#282)
by
unknown
01:43
created

pincer.objects.message.file   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 204
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 78
dl 0
loc 204
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A File.from_file() 0 27 2
A File.from_pillow_image() 0 54 4
A File.uri() 0 18 2

2 Functions

Rating   Name   Duplication   Size   Complexity  
A create_form() 0 29 3
A _get_file_extension() 0 17 2
1
# Copyright Pincer 2021-Present
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# Full MIT License can be found in `LICENSE` at the project root.
3
4
from __future__ import annotations
5
6
from base64 import b64encode
7
import os
8
from dataclasses import dataclass
9
from io import BytesIO
10
from json import dumps
11
from typing import TYPE_CHECKING
12
13
from aiohttp import FormData, Payload
14
15
from ...exceptions import ImageEncodingError
16
17
if TYPE_CHECKING:
18
    from typing import Any, Dict, List, Optional, Tuple
19
20
    IMAGE_TYPE = Any
21
22
PILLOW_IMPORT = True
23
24
25
try:
26
    from PIL.Image import Image
27
28
    if TYPE_CHECKING:
29
        IMAGE_TYPE = Image
30
except (ModuleNotFoundError, ImportError):
31
    PILLOW_IMPORT = False
32
33
34
def create_form(
35
    json_payload: Dict[Any], files: List[File]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
36
) -> Tuple[str, Payload]:
37
    """
38
    Creates a aiohttp payload from an array of File objects.
39
40
    json_payload : Dict[Any]
41
        The json part of the request
42
    files : List[`~pincer.objects.message.file.File`]
43
        A list of files to be used in the request.
44
45
    Returns
46
    -------
47
    Tuple[str, :class:`aiohttp.Payload`]
48
        The content type and the payload to be send in an HTTP request.
49
    """
50
    form = FormData()
51
    form.add_field("payload_json", dumps(json_payload))
52
53
    for file in files:
54
        if not file.filename:
55
            raise ImageEncodingError(
56
                "A filename is required for uploading attachments"
57
            )
58
59
        form.add_field("file", file.content, filename=file.filename)
60
61
    payload = form()
62
    return payload.headers["Content-Type"], payload
63
64
65
def _get_file_extension(filename: str) -> Optional[str]:
66
    """
67
    Returns the file extension from a str if it exists, otherwise
68
    return :data:`None`.
69
70
    filename : str
71
        The filename
72
73
    Returns
74
    -------
75
    Optional[:class:`str`]
76
        The file extension or :data:`None`
77
    """
78
    path = os.path.splitext(filename)
79
    if len(path) >= 2:
80
        return path[1][1:]
81
    return None
82
83
84
@dataclass
85
class File:
86
    """A file that is prepared by the user to be send to the discord
87
    API.
88
89
    Attributes
90
    ----------
91
    content: :class:`bytes`
92
        File bytes.
93
    filename: :class:`str`
94
        The name of the file when its uploaded to discord.
95
    """
96
97
    content: bytes
98
    image_format: str
99
    filename: Optional[str] = None
100
101
    @classmethod
102
    def from_file(cls, filepath: str, filename: str = None) -> File:
103
        """Make a ``File`` object from a file stored locally.
104
105
        Parameters
106
        ----------
107
        filepath: :class:`str`
108
            The path to the file you want to send. Must be string. The file's
109
            name in the file path is used as the name when uploaded to discord
110
            by default.
111
112
        filename: :class:`str`
113
            The name of the file. Will override the default name.
114
            |default| ``os.path.basename(filepath)``
115
116
        Returns
117
        -------
118
        :class:`~pincer.objects.message.file.File`
119
            The new file object.
120
        """
121
        with open(filepath, "rb") as data:
122
            file = data.read()
123
124
        return cls(
125
            content=file,
126
            image_format=_get_file_extension(filename),
127
            filename=filename or os.path.basename(filepath)
128
        )
129
130
    @classmethod
131
    def from_pillow_image(
132
            cls,
0 ignored issues
show
Unused Code introduced by
The argument kwargs seems to be unused.
Loading history...
133
            img: IMAGE_TYPE,
134
            filename: Optional[str] = None,
135
            image_format: Optional[str] = None,
136
            **kwargs
137
    ) -> File:
138
        """Creates a file object from a PIL image
139
        Supports GIF, PNG, JPEG, and WEBP.
140
141
        Parameters
142
        ----------
143
        img: :class:`~pil:PIL.Image.Image`
144
            Pillow image object.
145
        filename:
146
            The filename to be used when uploaded to discord. The extension is
147
            used as image_format unless otherwise specified.
148
        image_format:
149
            The image_format to be used if you want to override the file
150
            extension.
151
152
        Returns
153
        -------
154
        :class:`~pincer.objects.message.file.File`
155
            The new file object.
156
157
        Raises
158
        ------
159
        ModuleNotFoundError:
160
            ``Pillow`` is not installed
161
        """
162
        if not PILLOW_IMPORT:
163
            raise ModuleNotFoundError(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable ModuleNotFoundError does not seem to be defined.
Loading history...
164
                "The `Pillow` library is required for sending and converting "
165
                "pillow images,"
166
            )
167
168
        if image_format is None:
169
            image_format = _get_file_extension(filename)
170
171
        if image_format == "jpg":
172
            image_format = "jpeg"
173
174
        # https://stackoverflow.com/questions/33101935/convert-pil-image-to-byte-array
175
        # Credit goes to second answer
176
        img_byte_arr = BytesIO()
177
        img.save(img_byte_arr, format=image_format)
178
        img_bytes = img_byte_arr.getvalue()
179
180
        return cls(
181
            content=img_bytes,
182
            image_format=image_format,
183
            filename=filename
184
        )
185
186
    @property
187
    def uri(self) -> str:
188
        """
189
        Returns
190
        -------
191
        str
192
            The uri for the image.
193
            See `<https://discord.com/developers/docs/reference#api-versioning>`_.
194
        """  # noqa: E501
195
        if self.image_format not in {"jpeg", "png", "gif"}:
196
            raise ImageEncodingError(
197
                "Only image types \"jpeg\", \"png\", and \"gif\" can be sent in"
198
                " an Image URI"
199
            )
200
201
        encoded_bytes = b64encode(self.content).decode('ascii')
202
203
        return f"data:image/{self.image_format};base64,{encoded_bytes}"
204