python3-cyberfusion-nextcloud-support 1.1.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.1
2
+ Name: python3-cyberfusion-nextcloud-support
3
+ Version: 1.1.1.1
4
+ Summary: Library for NextCloud.
5
+ Home-page: https://github.com/CyberfusionIO/python3-cyberfusion-nextcloud-support
6
+ Author: Cyberfusion
7
+ Author-email: support@cyberfusion.io
8
+ Platform: linux
9
+ Description-Content-Type: text/markdown
10
+
11
+ # python3-cyberfusion-nextcloud-support
12
+
13
+ Library for NextCloud.
14
+
15
+ # Install
16
+
17
+ ## PyPI
18
+
19
+ Run the following command to install the package from PyPI:
20
+
21
+ pip3 install python3-cyberfusion-nextcloud-support
22
+
23
+ ## Generic
24
+
25
+ Run the following command to create a source distribution:
26
+
27
+ python3 setup.py sdist
28
+
29
+ Next, install the following software:
30
+
31
+ * PHP (see https://docs.nextcloud.com/server/latest/admin_manual/installation/php_configuration.html)
32
+
33
+ ## Debian
34
+
35
+ Run the following commands to build a Debian package:
36
+
37
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
38
+ dpkg-buildpackage -us -uc
39
+
40
+ # Configure
41
+
42
+ No configuration is supported.
43
+
44
+ # Usage
45
+
46
+ See code.
@@ -0,0 +1,36 @@
1
+ # python3-cyberfusion-nextcloud-support
2
+
3
+ Library for NextCloud.
4
+
5
+ # Install
6
+
7
+ ## PyPI
8
+
9
+ Run the following command to install the package from PyPI:
10
+
11
+ pip3 install python3-cyberfusion-nextcloud-support
12
+
13
+ ## Generic
14
+
15
+ Run the following command to create a source distribution:
16
+
17
+ python3 setup.py sdist
18
+
19
+ Next, install the following software:
20
+
21
+ * PHP (see https://docs.nextcloud.com/server/latest/admin_manual/installation/php_configuration.html)
22
+
23
+ ## Debian
24
+
25
+ Run the following commands to build a Debian package:
26
+
27
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
28
+ dpkg-buildpackage -us -uc
29
+
30
+ # Configure
31
+
32
+ No configuration is supported.
33
+
34
+ # Usage
35
+
36
+ See code.
@@ -0,0 +1,21 @@
1
+ [tool.isort]
2
+ profile = "black"
3
+ line_length = 79
4
+ known_first_party = ["cyberfusion"]
5
+ default_section = "THIRDPARTY"
6
+
7
+ [tool.black]
8
+ line-length = 79
9
+ exclude = '''
10
+ (
11
+ /(
12
+ \.eggs # exclude a few common directories in the
13
+ | \.git # root of the project
14
+ | \.hg
15
+ | \.mypy_cache
16
+ | \.tox
17
+ | \.venv
18
+ | venv
19
+ )/
20
+ )
21
+ '''
@@ -0,0 +1,12 @@
1
+ [aliases]
2
+ test = pytest
3
+
4
+ [tool:pytest]
5
+ norecursedirs = .git build dist *.egg __pycache__ .cache
6
+ testpaths = tests
7
+ junit_suite_name = python3-cyberfusion-nextcloud-support
8
+
9
+ [egg_info]
10
+ tag_build =
11
+ tag_date = 0
12
+
@@ -0,0 +1,23 @@
1
+ """A setuptools based setup module."""
2
+
3
+ from setuptools import setup
4
+
5
+ with open("README.md", "r", encoding="utf-8") as fh:
6
+ long_description = fh.read()
7
+
8
+ setup(
9
+ name="python3-cyberfusion-nextcloud-support",
10
+ version="1.1.1.1",
11
+ description="Library for NextCloud.",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ author="Cyberfusion",
15
+ author_email="support@cyberfusion.io",
16
+ url="https://github.com/CyberfusionIO/python3-cyberfusion-nextcloud-support",
17
+ platforms=["linux"],
18
+ packages=[
19
+ "cyberfusion.NextCloudSupport",
20
+ ],
21
+ package_dir={"": "src"},
22
+ data_files=[],
23
+ )
@@ -0,0 +1 @@
1
+ """Empty file to turn this into a package."""
@@ -0,0 +1,37 @@
1
+ """Functions to run `occ` commands."""
2
+
3
+ import subprocess
4
+ from typing import List
5
+
6
+ from cyberfusion.Common import find_executable
7
+ from cyberfusion.NextCloudSupport.exceptions import CommandFailedError
8
+
9
+ PHP_BIN = find_executable("php")
10
+
11
+
12
+ def run_command(command: List[str], cwd: str) -> str:
13
+ """Run command and get output."""
14
+ command = [
15
+ PHP_BIN,
16
+ "-d",
17
+ "memory_limit=512M",
18
+ "occ",
19
+ "--no-interaction",
20
+ ] + command
21
+
22
+ try:
23
+ return subprocess.run(
24
+ command,
25
+ check=True,
26
+ stdout=subprocess.PIPE,
27
+ stderr=subprocess.PIPE,
28
+ text=True,
29
+ cwd=cwd,
30
+ ).stdout.rstrip()
31
+ except subprocess.CalledProcessError as e:
32
+ raise CommandFailedError(
33
+ return_code=e.returncode,
34
+ stdout=e.stdout,
35
+ stderr=e.stderr,
36
+ command=command,
37
+ )
@@ -0,0 +1,143 @@
1
+ """App."""
2
+
3
+ import os
4
+ import re
5
+ import tarfile
6
+ from typing import TYPE_CHECKING, Optional, Tuple
7
+
8
+ from cyberfusion.Common import download_from_url
9
+ from cyberfusion.NextCloudSupport._occ import run_command
10
+ from cyberfusion.NextCloudSupport.exceptions import AppNotInstalledError
11
+
12
+ if TYPE_CHECKING: # pragma: no cover
13
+ from cyberfusion.NextCloudSupport.instance import Instance
14
+
15
+
16
+ class App:
17
+ """Represents app."""
18
+
19
+ def __init__(
20
+ self,
21
+ instance: "Instance",
22
+ name: str,
23
+ ) -> None:
24
+ """Set attributes."""
25
+ self.instance = instance
26
+ self.name = name
27
+
28
+ @staticmethod
29
+ def install(
30
+ instance: "Instance",
31
+ name: Optional[str] = None,
32
+ url: Optional[str] = None,
33
+ ) -> None:
34
+ """Install app by name or URL.
35
+
36
+ For custom versions: NextCloud does not natively support installing
37
+ specific versions of apps (default is latest). To work around this,
38
+ install the app by URL, pointing to the archive containing the
39
+ needed version.
40
+
41
+ Note that installing apps from a specific URL is not officially
42
+ supported, and the way we do it is undocumented, and therefore
43
+ a hack.
44
+ """
45
+ if name and url:
46
+ raise ValueError("Specify either name or URL")
47
+
48
+ if name:
49
+ run_command(["app:install", name], instance.path)
50
+
51
+ return
52
+
53
+ with tarfile.open(download_from_url(url)) as f:
54
+ name = os.path.commonpath(f.getnames())
55
+
56
+ f.extractall(path=os.path.join(instance.path, "apps"))
57
+
58
+ run_command(
59
+ ["app:enable", name],
60
+ instance.path,
61
+ )
62
+
63
+ @property
64
+ def is_enabled(self) -> bool:
65
+ """Get if app is enabled."""
66
+ return self.name in self.instance.raw_app_list["enabled"]
67
+
68
+ def enable(self) -> None:
69
+ """Enable app."""
70
+ run_command(
71
+ ["app:enable", self.name],
72
+ self.instance.path,
73
+ )
74
+
75
+ self.instance.refresh_raw_app_list()
76
+
77
+ def disable(self) -> None:
78
+ """Disable app."""
79
+ run_command(
80
+ ["app:disable", self.name],
81
+ self.instance.path,
82
+ )
83
+
84
+ self.instance.refresh_raw_app_list()
85
+
86
+ @property
87
+ def version(self) -> str:
88
+ """Get version."""
89
+ for app_name, version in (
90
+ self.instance.raw_app_list["enabled"]
91
+ | self.instance.raw_app_list["disabled"]
92
+ ).items():
93
+ if app_name != self.name:
94
+ continue
95
+
96
+ # Sometimes, NextCloud suffixes the version by another version number.
97
+ # It's unclear why or when, but we don't want it.
98
+ # Code: https://github.com/nextcloud/server/blob/72b6db40435ce0407d0aafa626945d4f2380460f/core/Command/App/ListApps.php#L91
99
+
100
+ return version.split(" ")[0]
101
+
102
+ raise AppNotInstalledError
103
+
104
+ def remove(self) -> None:
105
+ """Remove app."""
106
+ run_command(
107
+ ["app:remove", self.name],
108
+ self.instance.path,
109
+ )
110
+
111
+ self.instance.refresh_raw_app_list()
112
+
113
+ def update(self) -> Tuple[str, str]:
114
+ """Update app."""
115
+ old_version = self.version
116
+
117
+ run_command(
118
+ ["app:update", self.name],
119
+ self.instance.path,
120
+ )
121
+
122
+ self.instance.refresh_raw_app_list()
123
+ self.instance.refresh_raw_app_update_list()
124
+
125
+ new_version = self.version
126
+
127
+ return old_version, new_version
128
+
129
+ @property
130
+ def available_version(self) -> Optional[str]:
131
+ """Get version that app can be updated to."""
132
+ for line in self.instance.raw_app_update_list:
133
+ match = re.fullmatch("^(.*) new version available: (.*)$", line)
134
+
135
+ if not match:
136
+ continue
137
+
138
+ if match.group(1) != self.name:
139
+ continue
140
+
141
+ return match.group(2)
142
+
143
+ return None
@@ -0,0 +1,31 @@
1
+ """Exceptions."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import List
5
+
6
+
7
+ class DirectoryNotEmptyError(Exception):
8
+ """Directory is not empty."""
9
+
10
+ pass
11
+
12
+
13
+ class AppNotInstalledError(Exception):
14
+ """App should be installed, but is not."""
15
+
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class CommandFailedError(Exception):
21
+ """Command failed."""
22
+
23
+ command: List[str]
24
+ return_code: int
25
+ stdout: str
26
+ stderr: str
27
+
28
+ @property
29
+ def streams(self) -> str:
30
+ """Combine output streams."""
31
+ return f"Stdout:\n\n{self.stdout}\n\nStderr:\n\n{self.stderr}"
@@ -0,0 +1,392 @@
1
+ """Instance."""
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import zipfile
9
+ from enum import StrEnum
10
+ from functools import cached_property
11
+ from typing import List, Optional, Tuple, Union
12
+
13
+ from cyberfusion.Common import download_from_url
14
+ from cyberfusion.NextCloudSupport._occ import PHP_BIN, run_command
15
+ from cyberfusion.NextCloudSupport.app import App
16
+ from cyberfusion.NextCloudSupport.exceptions import (
17
+ AppNotInstalledError,
18
+ CommandFailedError,
19
+ DirectoryNotEmptyError,
20
+ )
21
+ from cyberfusion.NextCloudSupport.user import User
22
+
23
+ URL_ZIP_NEXTCLOUD = "https://download.nextcloud.com/server/releases/latest.zip"
24
+
25
+
26
+ class SSLMode(StrEnum):
27
+ """SSL modes."""
28
+
29
+ NONE = "none"
30
+ SSL = "ssl"
31
+ TLS = "tls"
32
+
33
+
34
+ class MailAccountAuthMethod(StrEnum):
35
+ """Auth methods for mail accounts."""
36
+
37
+ PASSWORD = "password"
38
+ XOAUTH2 = "xoauth2"
39
+
40
+
41
+ class DatabaseType(StrEnum):
42
+ """Database types."""
43
+
44
+ SQLITE = "sqlite"
45
+ MYSQL = "mysql"
46
+ PGSQL = "pgsql"
47
+ OCI = "oci"
48
+
49
+
50
+ class Instance:
51
+ """Represents NextCloud instance."""
52
+
53
+ def __init__(self, path: str) -> None:
54
+ """Set attributes."""
55
+ self.path = path
56
+
57
+ @staticmethod
58
+ def download(
59
+ destination_path: str, zip_path: Optional[str] = None
60
+ ) -> None:
61
+ """Download NextCloud to path.
62
+
63
+ If zip_path is not set, NextCloud is downloaded from their website.
64
+ """
65
+ if os.listdir(destination_path):
66
+ raise DirectoryNotEmptyError
67
+
68
+ # Downlaod ZIP from NextCloud if not specified
69
+
70
+ if zip_path is None:
71
+ zip_path = download_from_url(
72
+ URL_ZIP_NEXTCLOUD,
73
+ root_directory=destination_path,
74
+ )
75
+
76
+ # Extract ZIP
77
+
78
+ with zipfile.ZipFile(zip_path, "r") as z:
79
+ z.extractall(destination_path)
80
+
81
+ # Move files from nextcloud/ to destination directory
82
+
83
+ temp_directory = os.path.join(
84
+ destination_path,
85
+ "nextcloud",
86
+ )
87
+
88
+ for file_ in os.listdir(temp_directory):
89
+ shutil.move(os.path.join(temp_directory, file_), destination_path)
90
+
91
+ os.rmdir(temp_directory)
92
+
93
+ @staticmethod
94
+ def install(
95
+ path: str,
96
+ *,
97
+ database_host: str,
98
+ database_name: str,
99
+ database_username: str,
100
+ database_password: str,
101
+ admin_user: str,
102
+ admin_password: str,
103
+ database_type: DatabaseType = DatabaseType.MYSQL,
104
+ ) -> None:
105
+ """Install downloaded NextCloud instance.
106
+
107
+ NextCloud must be downloaded before calling this method.
108
+ """
109
+ run_command(
110
+ [
111
+ "maintenance:install",
112
+ "--database",
113
+ database_type,
114
+ "--database-host",
115
+ database_host,
116
+ "--database-name",
117
+ database_name,
118
+ "--database-user",
119
+ database_username,
120
+ "--database-pass",
121
+ database_password,
122
+ "--admin-user",
123
+ admin_user,
124
+ "--admin-pass",
125
+ admin_password,
126
+ "--data-dir",
127
+ os.path.join(path, "data"),
128
+ ],
129
+ path,
130
+ )
131
+
132
+ def get_app(self, name: str) -> App:
133
+ """Get installed app by name."""
134
+ for app in self.installed_apps:
135
+ if name != app.name:
136
+ continue
137
+
138
+ return app
139
+
140
+ raise AppNotInstalledError
141
+
142
+ def get_system_config(self, name: str) -> Union[
143
+ str,
144
+ int,
145
+ float,
146
+ bool,
147
+ ]:
148
+ """Get system config value by name."""
149
+ output = run_command(
150
+ [
151
+ "config:system:get",
152
+ name,
153
+ ],
154
+ self.path,
155
+ )
156
+
157
+ if output.isdigit():
158
+ return int(output)
159
+
160
+ if output == "true":
161
+ return True
162
+
163
+ if output == "false":
164
+ return False
165
+
166
+ try:
167
+ return float(output)
168
+ except ValueError:
169
+ pass
170
+
171
+ return output
172
+
173
+ def set_system_config(
174
+ self,
175
+ name: str,
176
+ value: Union[str, int, float, bool],
177
+ index: Optional[int] = None,
178
+ ) -> None:
179
+ """Set system config value.
180
+
181
+ Index must be set when manipulating arrays, as it corresponds to the
182
+ array item.
183
+ """
184
+
185
+ # Set type
186
+
187
+ type_ = "string"
188
+
189
+ if isinstance(value, int):
190
+ type_ = "integer"
191
+
192
+ if isinstance(value, float):
193
+ type_ = "float"
194
+
195
+ if isinstance(value, bool):
196
+ type_ = "boolean"
197
+
198
+ # Set value
199
+
200
+ _value = str(value)
201
+
202
+ if isinstance(value, bool):
203
+ _value = _value.lower()
204
+
205
+ # Set command
206
+
207
+ command = [
208
+ "config:system:set",
209
+ name,
210
+ ]
211
+
212
+ if index is not None:
213
+ command.append(str(index))
214
+
215
+ command.extend(["--value", _value, "--type", type_])
216
+
217
+ # Run command
218
+
219
+ run_command(command, self.path)
220
+
221
+ def update(self) -> Tuple[str, str]:
222
+ """Update NextCloud."""
223
+ old_version = self.version
224
+
225
+ command = [PHP_BIN, "updater/updater.phar", "--no-interaction"]
226
+
227
+ try:
228
+ subprocess.run(
229
+ command,
230
+ check=True,
231
+ stdout=subprocess.PIPE,
232
+ stderr=subprocess.PIPE,
233
+ cwd=self.path,
234
+ )
235
+ except subprocess.CalledProcessError as e:
236
+ raise CommandFailedError(
237
+ return_code=e.returncode,
238
+ stdout=e.stdout,
239
+ stderr=e.stderr,
240
+ command=command,
241
+ )
242
+
243
+ new_version = self.version
244
+
245
+ return old_version, new_version
246
+
247
+ @property
248
+ def available_version(self) -> Optional[str]:
249
+ """Get version that instance can be updated to."""
250
+ lines = run_command(
251
+ [
252
+ "update:check",
253
+ ],
254
+ self.path,
255
+ ).splitlines()
256
+
257
+ for line in lines:
258
+ match = re.fullmatch(
259
+ "^Nextcloud (.*) is available. Get more information on how to update at (.*).$",
260
+ line,
261
+ )
262
+
263
+ if not match:
264
+ continue
265
+
266
+ return match.group(1)
267
+
268
+ return None
269
+
270
+ @property
271
+ def version(self) -> str:
272
+ """Get version."""
273
+ return self.get_system_config("version") # type: ignore[return-value]
274
+
275
+ def create_mail_account(
276
+ self,
277
+ *,
278
+ user_id: str,
279
+ name: str,
280
+ email_address: str,
281
+ imap_hostname: str,
282
+ imap_port: int,
283
+ imap_ssl_mode: SSLMode,
284
+ imap_username: str,
285
+ imap_password: str,
286
+ smtp_host: str,
287
+ smtp_port: int,
288
+ smtp_ssl_mode: SSLMode,
289
+ smtp_username: str,
290
+ smtp_password: str,
291
+ auth_method: MailAccountAuthMethod,
292
+ ) -> None:
293
+ """Create mail account."""
294
+ run_command(
295
+ [
296
+ "mail:account:create",
297
+ user_id,
298
+ name,
299
+ email_address,
300
+ imap_hostname,
301
+ str(imap_port),
302
+ imap_ssl_mode,
303
+ imap_username,
304
+ imap_password,
305
+ smtp_host,
306
+ str(smtp_port),
307
+ smtp_ssl_mode,
308
+ smtp_username,
309
+ smtp_password,
310
+ auth_method,
311
+ ],
312
+ self.path,
313
+ )
314
+
315
+ @property
316
+ def users(self) -> List[User]:
317
+ """Get users."""
318
+ result = []
319
+
320
+ output = json.loads(
321
+ run_command(
322
+ [
323
+ "user:list",
324
+ "--output",
325
+ "json",
326
+ ],
327
+ self.path,
328
+ )
329
+ )
330
+
331
+ for id_, name in output.items():
332
+ user = User(self, id_, name)
333
+
334
+ result.append(user)
335
+
336
+ return result
337
+
338
+ def refresh_raw_app_list(self) -> None:
339
+ """Clear the raw app list cache."""
340
+ try:
341
+ del self.raw_app_list
342
+ except AttributeError:
343
+ pass
344
+
345
+ def refresh_raw_app_update_list(self) -> None:
346
+ """Clear the raw app list cache."""
347
+ try:
348
+ del self.raw_app_update_list
349
+ except AttributeError:
350
+ pass
351
+
352
+ @cached_property
353
+ def raw_app_update_list(self) -> List[str]:
354
+ """Get raw app list output."""
355
+ return run_command(
356
+ [
357
+ "app:update",
358
+ "--showonly",
359
+ ],
360
+ self.path,
361
+ ).splitlines()
362
+
363
+ @cached_property
364
+ def raw_app_list(self) -> dict:
365
+ """Get raw app list output."""
366
+ return json.loads(
367
+ run_command(
368
+ [
369
+ "app:list",
370
+ "--output",
371
+ "json",
372
+ ],
373
+ self.path,
374
+ )
375
+ )
376
+
377
+ @property
378
+ def installed_apps(self) -> List[App]:
379
+ """Get installed apps."""
380
+ result = []
381
+
382
+ for name, _ in (
383
+ self.raw_app_list["enabled"] | self.raw_app_list["disabled"]
384
+ ).items():
385
+ app = App(
386
+ self,
387
+ name,
388
+ )
389
+
390
+ result.append(app)
391
+
392
+ return result
@@ -0,0 +1,16 @@
1
+ """User."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING: # pragma: no cover
6
+ from cyberfusion.NextCloudSupport.instance import Instance
7
+
8
+
9
+ class User:
10
+ """Represents user."""
11
+
12
+ def __init__(self, instance: "Instance", id_: str, name: str) -> None:
13
+ """Set attributes."""
14
+ self.instance = instance
15
+ self.id = id_
16
+ self.name = name
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.1
2
+ Name: python3-cyberfusion-nextcloud-support
3
+ Version: 1.1.1.1
4
+ Summary: Library for NextCloud.
5
+ Home-page: https://github.com/CyberfusionIO/python3-cyberfusion-nextcloud-support
6
+ Author: Cyberfusion
7
+ Author-email: support@cyberfusion.io
8
+ Platform: linux
9
+ Description-Content-Type: text/markdown
10
+
11
+ # python3-cyberfusion-nextcloud-support
12
+
13
+ Library for NextCloud.
14
+
15
+ # Install
16
+
17
+ ## PyPI
18
+
19
+ Run the following command to install the package from PyPI:
20
+
21
+ pip3 install python3-cyberfusion-nextcloud-support
22
+
23
+ ## Generic
24
+
25
+ Run the following command to create a source distribution:
26
+
27
+ python3 setup.py sdist
28
+
29
+ Next, install the following software:
30
+
31
+ * PHP (see https://docs.nextcloud.com/server/latest/admin_manual/installation/php_configuration.html)
32
+
33
+ ## Debian
34
+
35
+ Run the following commands to build a Debian package:
36
+
37
+ mk-build-deps -i -t 'apt -o Debug::pkgProblemResolver=yes --no-install-recommends -y'
38
+ dpkg-buildpackage -us -uc
39
+
40
+ # Configure
41
+
42
+ No configuration is supported.
43
+
44
+ # Usage
45
+
46
+ See code.
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ setup.py
5
+ src/cyberfusion/NextCloudSupport/__init__.py
6
+ src/cyberfusion/NextCloudSupport/_occ.py
7
+ src/cyberfusion/NextCloudSupport/app.py
8
+ src/cyberfusion/NextCloudSupport/exceptions.py
9
+ src/cyberfusion/NextCloudSupport/instance.py
10
+ src/cyberfusion/NextCloudSupport/user.py
11
+ src/python3_cyberfusion_nextcloud_support.egg-info/PKG-INFO
12
+ src/python3_cyberfusion_nextcloud_support.egg-info/SOURCES.txt
13
+ src/python3_cyberfusion_nextcloud_support.egg-info/dependency_links.txt
14
+ src/python3_cyberfusion_nextcloud_support.egg-info/top_level.txt