rbx 3.18.4.dev162__tar.gz → 3.18.5__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.
Files changed (51) hide show
  1. {rbx-3.18.4.dev162 → rbx-3.18.5}/PKG-INFO +1 -8
  2. {rbx-3.18.4.dev162 → rbx-3.18.5}/pyproject.toml +2 -12
  3. rbx-3.18.5/rbx/__init__.py +1 -0
  4. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/PKG-INFO +1 -8
  5. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/SOURCES.txt +0 -6
  6. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/entry_points.txt +0 -1
  7. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/requires.txt +0 -9
  8. rbx-3.18.4.dev162/rbx/__init__.py +0 -1
  9. rbx-3.18.4.dev162/rbx/toolkit/__init__.py +0 -79
  10. rbx-3.18.4.dev162/rbx/toolkit/browser.py +0 -200
  11. rbx-3.18.4.dev162/rbx/toolkit/cli.py +0 -108
  12. rbx-3.18.4.dev162/rbx/toolkit/exporter.py +0 -53
  13. rbx-3.18.4.dev162/rbx/toolkit/media.py +0 -45
  14. rbx-3.18.4.dev162/rbx/toolkit/utils.py +0 -72
  15. {rbx-3.18.4.dev162 → rbx-3.18.5}/LICENSE +0 -0
  16. {rbx-3.18.4.dev162 → rbx-3.18.5}/README.md +0 -0
  17. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/auth/__init__.py +0 -0
  18. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/auth/decorators.py +0 -0
  19. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/auth/keystore.py +0 -0
  20. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/auth/mock.py +0 -0
  21. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/aws/__init__.py +0 -0
  22. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/aws/s3.py +0 -0
  23. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/__init__.py +0 -0
  24. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/cli.py +0 -0
  25. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/tasks/__init__.py +0 -0
  26. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/tasks/apprunner.py +0 -0
  27. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/tasks/ec2.py +0 -0
  28. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/tasks/image.py +0 -0
  29. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/buildtools/tasks/misc.py +0 -0
  30. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/__init__.py +0 -0
  31. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/adsquare.py +0 -0
  32. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/broadsign.py +0 -0
  33. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/oxr.py +0 -0
  34. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/panels.py +0 -0
  35. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/reporting.py +0 -0
  36. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/clients/retry.py +0 -0
  37. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/exceptions.py +0 -0
  38. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/gcp/__init__.py +0 -0
  39. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/gcp/cloud_tasks.py +0 -0
  40. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/gcp/pubsub.py +0 -0
  41. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/gcp/storage.py +0 -0
  42. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/logging.py +0 -0
  43. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/settings.py +0 -0
  44. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/utils/__init__.py +0 -0
  45. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/utils/mdm.py +0 -0
  46. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/utils/vast.py +0 -0
  47. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/web/__init__.py +0 -0
  48. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx/web/handlers.py +0 -0
  49. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/dependency_links.txt +0 -0
  50. {rbx-3.18.4.dev162 → rbx-3.18.5}/rbx.egg-info/top_level.txt +0 -0
  51. {rbx-3.18.4.dev162 → rbx-3.18.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rbx
3
- Version: 3.18.4.dev162
3
+ Version: 3.18.5
4
4
  Summary: A collection of common tools for Scoota services.
5
5
  Author-email: The Scoota Engineering Team <engineering@scoota.com>
6
6
  License:
@@ -249,19 +249,12 @@ Requires-Dist: check-manifest; extra == "buildtools"
249
249
  Requires-Dist: fabric~=3.2.0; extra == "buildtools"
250
250
  Requires-Dist: Jinja2==3.1.2; extra == "buildtools"
251
251
  Requires-Dist: twine; extra == "buildtools"
252
- Provides-Extra: exporter
253
- Requires-Dist: starlette==0.38.2; extra == "exporter"
254
252
  Provides-Extra: notifications
255
253
  Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "notifications"
256
254
  Provides-Extra: storage
257
255
  Requires-Dist: google-cloud-storage<3,>=2.1; extra == "storage"
258
256
  Provides-Extra: tasks
259
257
  Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
260
- Provides-Extra: toolkit
261
- Requires-Dist: boto3; extra == "toolkit"
262
- Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
263
- Requires-Dist: playwright~=1.51.0; extra == "toolkit"
264
- Requires-Dist: sh~=2.0.6; extra == "toolkit"
265
258
  Provides-Extra: web
266
259
  Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
267
260
  Dynamic: license-file
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rbx"
7
- version = "3.18.4.dev162"
7
+ version = "3.18.5"
8
8
  description = "A collection of common tools for Scoota services."
9
9
  authors = [
10
10
  { name = "The Scoota Engineering Team", email = "engineering@scoota.com" }
@@ -59,9 +59,6 @@ buildtools = [
59
59
  "Jinja2==3.1.2",
60
60
  "twine",
61
61
  ]
62
- exporter = [
63
- "starlette==0.38.2",
64
- ]
65
62
  notifications = [
66
63
  "google-cloud-pubsub>=2.9,<3"
67
64
  ]
@@ -71,26 +68,19 @@ storage = [
71
68
  tasks = [
72
69
  "google-cloud-tasks>=2.7,<3"
73
70
  ]
74
- toolkit = [
75
- "boto3",
76
- "google-cloud-storage>=2.1,<3",
77
- "playwright~=1.51.0",
78
- "sh~=2.0.6",
79
- ]
80
71
  web = [
81
72
  "google-cloud-error-reporting>=1.9.2,<2"
82
73
  ]
83
74
 
84
75
  [project.scripts]
85
76
  buildtools = "rbx.buildtools.cli:cli [buildtools]"
86
- toolkit = "rbx.toolkit.cli:cli [toolkit]"
87
77
 
88
78
  [project.urls]
89
79
  homepage = "https://github.com/rockabox/rbx"
90
80
  repository = "https://github.com/rockabox/rbx.git"
91
81
 
92
82
  [tool.bumpversion]
93
- current_version = "3.18.4.dev162"
83
+ current_version = "3.18.5"
94
84
  commit = true
95
85
  parse = """
96
86
  (?P<major>\\d+)\\.
@@ -0,0 +1 @@
1
+ __version__ = "3.18.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rbx
3
- Version: 3.18.4.dev162
3
+ Version: 3.18.5
4
4
  Summary: A collection of common tools for Scoota services.
5
5
  Author-email: The Scoota Engineering Team <engineering@scoota.com>
6
6
  License:
@@ -249,19 +249,12 @@ Requires-Dist: check-manifest; extra == "buildtools"
249
249
  Requires-Dist: fabric~=3.2.0; extra == "buildtools"
250
250
  Requires-Dist: Jinja2==3.1.2; extra == "buildtools"
251
251
  Requires-Dist: twine; extra == "buildtools"
252
- Provides-Extra: exporter
253
- Requires-Dist: starlette==0.38.2; extra == "exporter"
254
252
  Provides-Extra: notifications
255
253
  Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "notifications"
256
254
  Provides-Extra: storage
257
255
  Requires-Dist: google-cloud-storage<3,>=2.1; extra == "storage"
258
256
  Provides-Extra: tasks
259
257
  Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
260
- Provides-Extra: toolkit
261
- Requires-Dist: boto3; extra == "toolkit"
262
- Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
263
- Requires-Dist: playwright~=1.51.0; extra == "toolkit"
264
- Requires-Dist: sh~=2.0.6; extra == "toolkit"
265
258
  Provides-Extra: web
266
259
  Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
267
260
  Dynamic: license-file
@@ -35,12 +35,6 @@ rbx/gcp/__init__.py
35
35
  rbx/gcp/cloud_tasks.py
36
36
  rbx/gcp/pubsub.py
37
37
  rbx/gcp/storage.py
38
- rbx/toolkit/__init__.py
39
- rbx/toolkit/browser.py
40
- rbx/toolkit/cli.py
41
- rbx/toolkit/exporter.py
42
- rbx/toolkit/media.py
43
- rbx/toolkit/utils.py
44
38
  rbx/utils/__init__.py
45
39
  rbx/utils/mdm.py
46
40
  rbx/utils/vast.py
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  buildtools = rbx.buildtools.cli:cli [buildtools]
3
- toolkit = rbx.toolkit.cli:cli [toolkit]
@@ -29,9 +29,6 @@ fabric~=3.2.0
29
29
  Jinja2==3.1.2
30
30
  twine
31
31
 
32
- [exporter]
33
- starlette==0.38.2
34
-
35
32
  [notifications]
36
33
  google-cloud-pubsub<3,>=2.9
37
34
 
@@ -41,11 +38,5 @@ google-cloud-storage<3,>=2.1
41
38
  [tasks]
42
39
  google-cloud-tasks<3,>=2.7
43
40
 
44
- [toolkit]
45
- boto3
46
- google-cloud-storage<3,>=2.1
47
- playwright~=1.51.0
48
- sh~=2.0.6
49
-
50
41
  [web]
51
42
  google-cloud-error-reporting<2,>=1.9.2
@@ -1 +0,0 @@
1
- __version__ = "3.18.4.dev162"
@@ -1,79 +0,0 @@
1
- __all__ = ["Options", "run"]
2
-
3
- import logging
4
- import tempfile
5
- from pathlib import Path
6
- from typing import NamedTuple, Optional
7
-
8
- from .browser import record, screenshot
9
- from .utils import upload
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class Options(NamedTuple):
15
- url: str
16
- width: int
17
- height: int
18
- format: str
19
- duration: Optional[int] = 0
20
- path: Optional[str] = "/tmp"
21
- output: Optional[str] = "."
22
- filename: Optional[str] = None
23
-
24
-
25
- def filename(output, filename):
26
- if output.startswith("s3://"):
27
- return "s3://" + str(Path(output[5:]) / filename)
28
- elif output.startswith("gs://"):
29
- return "gs://" + str(Path(output[5:]) / filename)
30
- return str(Path(output) / filename)
31
-
32
-
33
- async def capture(filename: str, options: Options, path: Path) -> None:
34
- await record(
35
- dirname=str(path),
36
- filename=str(path / filename),
37
- duration=options.duration,
38
- height=options.height,
39
- url=options.url,
40
- width=options.width,
41
- )
42
-
43
-
44
- async def screengrab(filename: str, options: Options, path: Path) -> None:
45
- await screenshot(
46
- filename=str(path / filename),
47
- height=options.height,
48
- url=options.url,
49
- width=options.width,
50
- )
51
-
52
-
53
- async def run(options: Options) -> None:
54
- with tempfile.TemporaryDirectory(dir=options.path) as dirname:
55
- path = Path(dirname)
56
- logger.debug(f"Working directory: '{path}'")
57
- if options.format == "video":
58
- asset = options.filename or "video.mp4"
59
- output = filename(options.output, asset)
60
- if options.duration:
61
- logger.info(
62
- f"Capturing '{options.url}' [{options.width}x{options.height}]"
63
- f" for {options.duration}ms to '{output}'",
64
- )
65
- else:
66
- logger.info(
67
- f"Capturing '{options.url}' [{options.width}x{options.height}] to '{output}'",
68
- )
69
- await capture(filename=asset, options=options, path=path)
70
- else:
71
- asset = options.filename or "screenshot.png"
72
- output = filename(options.output, asset)
73
- logger.info(
74
- f"Taking screenshot of '{options.url}' [{options.width}x{options.height}]"
75
- f" to '{output}'"
76
- )
77
- await screengrab(filename=asset, options=options, path=path)
78
-
79
- upload(path / asset, output)
@@ -1,200 +0,0 @@
1
- import datetime
2
- import logging
3
- import os
4
- import signal
5
- import subprocess
6
- import time
7
- import uuid
8
-
9
- from playwright.async_api import async_playwright
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- def get_free_display(base=99, max_tries=100):
15
- for i in range(max_tries):
16
- display = f":{base + i}"
17
- if not os.path.exists(f"/tmp/.X{base + i}-lock"):
18
- return display
19
-
20
- raise RuntimeError("No free display found")
21
-
22
-
23
- class RecordingSession:
24
- def __init__(self, dirname, width, height):
25
- self.display = get_free_display()
26
- self.dirname = dirname
27
- self.width = width
28
- self.height = height
29
- self.resolution = f"{self.width}x{self.height}"
30
- self.session_id = str(uuid.uuid4())[:8]
31
- self.sink_name = f"rec_sink_{self.session_id}"
32
- self.pulse_dir = f"{dirname}/pulseaudio"
33
- self.pulse_socket = os.path.join(self.pulse_dir, "pulse-socket")
34
- self.proc = []
35
-
36
- async def __aenter__(self):
37
- return self
38
-
39
- async def __aexit__(self, exc_type, exc_val, exc_tb):
40
- logger.debug("Stopping processes...")
41
-
42
- # PulseAudio needs to be terminated via the kill command.
43
- subprocess.Popen(
44
- ["pulseaudio", "--kill"],
45
- env={
46
- "PULSE_SERVER": f"unix:{self.pulse_socket}",
47
- "XDG_RUNTIME_DIR": self.pulse_dir,
48
- },
49
- )
50
-
51
- for proc in reversed(self.proc):
52
- if proc and proc.poll() is None:
53
- try:
54
- proc.send_signal(signal.SIGINT)
55
- proc.wait(timeout=3)
56
- except subprocess.TimeoutExpired:
57
- proc.kill()
58
-
59
- def start_xvfb(self):
60
- logger.debug(f"Starting Xvfb [{self.display}]...")
61
- # fmt: off
62
- self.proc.append(
63
- subprocess.Popen(
64
- [
65
- "Xvfb", self.display, "-screen", "0", f"{self.resolution}x24",
66
- # These options are needed when running in a container
67
- "-ac", "-nolisten", "tcp", "-nolisten", "unix",
68
- ]
69
- )
70
- )
71
- # fmt: on
72
- time.sleep(1)
73
-
74
- logger.debug(f"Starting unclutter [{self.display}]...")
75
- self.proc.append(
76
- subprocess.Popen(
77
- ["unclutter", "-idle", "0"],
78
- env={"DISPLAY": self.display},
79
- )
80
- )
81
-
82
- def start_pulseaudio(self):
83
- logger.debug(f"Starting PulseAudio [{self.pulse_dir}.{self.sink_name}]...")
84
- os.makedirs(self.pulse_dir, exist_ok=True)
85
- os.chmod(self.pulse_dir, 0o700) # PulseAudio requires strict permissions
86
-
87
- subprocess.Popen(
88
- [
89
- "pulseaudio",
90
- "--start",
91
- "--disable-shm",
92
- "--exit-idle-time=300",
93
- f"--load=module-native-protocol-unix socket={self.pulse_socket}",
94
- f"--load=module-null-sink sink_name={self.sink_name}"
95
- f" sink_properties=device.description={self.sink_name}",
96
- ],
97
- env={
98
- "DISPLAY": self.display,
99
- "PULSE_NO_SIMD": "1",
100
- "XDG_RUNTIME_DIR": self.pulse_dir,
101
- },
102
- )
103
- time.sleep(2)
104
-
105
- def start_ffmpeg(self, duration, output_path):
106
- logger.debug(f"Starting FFmpeg recording [{self.display}:{self.sink_name}]...")
107
- # fmt: off
108
- self.proc.append(
109
- subprocess.Popen(
110
- [
111
- "ffmpeg", "-y",
112
- "-t", str(duration),
113
- "-video_size", self.resolution,
114
- "-f", "x11grab",
115
- "-i", f"{self.display}.0",
116
- "-f", "pulse",
117
- "-i", f"{self.sink_name}.monitor",
118
- "-c:v", "libx264",
119
- "-preset", "slow",
120
- "-crf", "8",
121
- "-pix_fmt", "yuv420p",
122
- "-c:a", "aac",
123
- "-b:a", "256k",
124
- "-movflags",
125
- "+faststart",
126
- output_path,
127
- ],
128
- stdout=subprocess.DEVNULL,
129
- stderr=subprocess.STDOUT,
130
- env={
131
- "PULSE_SERVER": f"unix:{self.pulse_socket}",
132
- "XDG_RUNTIME_DIR": self.pulse_dir,
133
- },
134
- )
135
- )
136
- # fmt: on
137
-
138
- async def capture(self, url, duration, output_path):
139
- self.start_xvfb()
140
- self.start_pulseaudio()
141
-
142
- async with async_playwright() as p:
143
- logger.debug(
144
- f"Capturing Chromium session [{self.display}:{self.sink_name}]..."
145
- )
146
- context = await p.chromium.launch_persistent_context(
147
- user_data_dir=f"{self.dirname}/user-data",
148
- headless=False,
149
- channel="chrome",
150
- args=[
151
- "--no-sandbox",
152
- "--disable-gpu",
153
- "--disable-software-rasterizer",
154
- f"--display={self.display}",
155
- "--autoplay-policy=no-user-gesture-required",
156
- "--use-fake-ui-for-media-stream",
157
- f"--alsa-output-device={self.sink_name}",
158
- "--start-fullscreen",
159
- "--window-position=0,0",
160
- f"--window-size={self.width},{self.height}",
161
- ],
162
- ignore_default_args=["--enable-automation"],
163
- no_viewport=True,
164
- env={
165
- "PULSE_SERVER": f"unix:{self.pulse_socket}",
166
- "XDG_RUNTIME_DIR": self.pulse_dir,
167
- },
168
- )
169
- page = await context.new_page()
170
- await page.goto(url, wait_until="commit")
171
-
172
- logger.info(
173
- f"Recording session [{self.display}:{self.sink_name}] for {duration}ms"
174
- )
175
-
176
- self.start_ffmpeg(duration, output_path)
177
-
178
- await page.wait_for_timeout(duration) # Wait for the duration
179
- await context.close()
180
-
181
-
182
- async def record(
183
- dirname: str, filename: str, duration: int, height: int, url: str, width: int
184
- ) -> None:
185
- async with RecordingSession(dirname, width, height) as session:
186
- await session.capture(url, duration, filename)
187
-
188
-
189
- async def screenshot(filename: str, height: int, url: str, width: int) -> None:
190
- async with async_playwright() as p:
191
- browser = await p.chromium.launch()
192
- page = await browser.new_page()
193
- await page.set_viewport_size({"width": width, "height": height})
194
- await page.goto(url, wait_until="networkidle")
195
- await page.screenshot(path=filename)
196
- await browser.close()
197
-
198
-
199
- def to_ms(timedelta: datetime.timedelta) -> int:
200
- return round(1000 * timedelta.total_seconds())
@@ -1,108 +0,0 @@
1
- import asyncio
2
- import datetime
3
- import logging
4
- import logging.config
5
- import os
6
-
7
- import click
8
-
9
- from . import Options, run
10
-
11
-
12
- class ColourFormatter(logging.Formatter):
13
- def format(self, record):
14
- message = super().format(record)
15
- colours = {
16
- logging.INFO: {"bold": True},
17
- logging.WARNING: {"fg": "yellow"},
18
- logging.ERROR: {"fg": "bright_red"},
19
- logging.CRITICAL: {"fg": "bright_white", "bg": "red"},
20
- }
21
- try:
22
- message = click.style(message, **colours[record.levelno])
23
- except KeyError:
24
- pass
25
- return message
26
-
27
-
28
- @click.group()
29
- @click.version_option(message="%(version)s")
30
- def cli():
31
- """Toolkit Creative Converter."""
32
-
33
-
34
- def validate_filename(ctx, param, value):
35
- if value is None:
36
- return
37
-
38
- if ctx.params["format"] == "image" and not value.endswith(".png"):
39
- raise click.BadParameter(f"{param.name} must use the '.png' extension")
40
- elif ctx.params["format"] == "video" and not value.endswith(".mp4"):
41
- raise click.BadParameter(f"{param.name} must use the '.mp4' extension")
42
-
43
- return value
44
-
45
-
46
- @cli.command(context_settings={"show_default": True})
47
- @click.argument("url")
48
- @click.option("--width", "-w", type=click.INT, default=1080)
49
- @click.option("--height", "-h", type=click.INT, default=1920)
50
- @click.option(
51
- "--format",
52
- type=click.Choice(["image", "video"], case_sensitive=False),
53
- default="image",
54
- )
55
- @click.option("--duration", "-d", type=click.INT, default=0)
56
- @click.option("--path", "-p", default="/tmp")
57
- @click.option("--output-dir", "-o", type=click.Path(file_okay=False), default=".")
58
- @click.option("--filename", "-f", callback=validate_filename)
59
- def export(url, width, height, format, duration, path, output_dir, filename):
60
- log_level = os.getenv("LOG_LEVEL", "INFO")
61
- logging.config.dictConfig(
62
- {
63
- "version": 1,
64
- "formatters": {
65
- "coloured": {
66
- "class": "rbx.toolkit.cli.ColourFormatter",
67
- "format": "[%(asctime)s] %(message)s",
68
- "datefmt": "%Y-%m-%d %H:%M:%S",
69
- },
70
- },
71
- "handlers": {
72
- "console": {
73
- "level": "DEBUG",
74
- "class": "logging.StreamHandler",
75
- "formatter": "coloured",
76
- },
77
- },
78
- "loggers": {
79
- "rbx.toolkit": {
80
- "level": log_level,
81
- "handlers": ["console"],
82
- "propagate": False,
83
- },
84
- },
85
- }
86
- )
87
-
88
- start_timestamp = datetime.datetime.now()
89
-
90
- try:
91
- asyncio.run(
92
- run(
93
- options=Options(
94
- url=url,
95
- width=width,
96
- height=height,
97
- format=format,
98
- duration=duration,
99
- path=path,
100
- output=output_dir,
101
- filename=filename,
102
- )
103
- )
104
- )
105
- except RuntimeError as e:
106
- click.secho(e, fg="red", bold=True)
107
-
108
- click.secho(f"Finished in {datetime.datetime.now() - start_timestamp}", bold=True)
@@ -1,53 +0,0 @@
1
- import logging
2
-
3
- from starlette.applications import Starlette
4
- from starlette.config import Config
5
- from starlette.responses import JSONResponse
6
- from starlette.routing import Route
7
-
8
- from . import Options, run
9
-
10
- config = Config()
11
- logging.basicConfig(level=config("LOG_LEVEL", default="INFO"))
12
-
13
-
14
- async def handler(request):
15
- payload = await request.json()
16
-
17
- errors = []
18
- for param in ("url", "width", "height", "format", "name"):
19
- if param not in payload.keys():
20
- errors.append(f"Missing required '{param}' parameter")
21
-
22
- if payload.get("format") == "video" and "duration" not in payload.keys():
23
- errors.append("Missing required 'duration' parameter")
24
-
25
- if errors:
26
- return JSONResponse({"errors": errors}, status_code=400)
27
-
28
- ext = "mp4" if payload["format"] == "video" else "png"
29
- filename = f"{payload['name']}.{ext}"
30
- project_id = config("GOOGLE_CLOUD_PROJECT", default="dev-platform-eu")
31
- output = f"gs://{project_id}.appspot.com/toolkit/exports/"
32
-
33
- await run(
34
- options=Options(
35
- url=payload["url"],
36
- width=payload["width"],
37
- height=payload["height"],
38
- format=payload["format"],
39
- duration=int(payload.get("duration", 0)),
40
- output=output,
41
- filename=filename,
42
- )
43
- )
44
-
45
- return JSONResponse({"path": f"{output}{filename}"})
46
-
47
-
48
- def create_app() -> Starlette:
49
- return Starlette(
50
- routes=[
51
- Route("/", endpoint=handler, methods=["POST"]),
52
- ]
53
- )
@@ -1,45 +0,0 @@
1
- from typing import Optional
2
-
3
- import sh
4
-
5
-
6
- def convert(
7
- infile: str,
8
- outfile: str,
9
- delay: Optional[int] = 0,
10
- duration: Optional[int] = 0,
11
- opts: Optional[list] = None,
12
- ):
13
- """Convert `infile` to `outfile`.
14
-
15
- For WebM to MP4 conversion, see
16
- https://blog.addpipe.com/converting-webm-to-mp4-with-ffmpeg/
17
-
18
- """
19
- args = ["-y"]
20
- args.extend(["-i", infile])
21
- if delay:
22
- args.extend(["-ss", milliseconds_to_duration(delay)])
23
- if duration:
24
- args.extend(["-to", milliseconds_to_duration(delay + duration)])
25
- if delay or duration:
26
- args.extend(["-c:v", "libx264", "-preset", "slow", "-crf", "8"])
27
- if opts:
28
- args.extend(opts)
29
- args.append(outfile)
30
- ffmpg(args)
31
-
32
-
33
- def ffmpg(args):
34
- try:
35
- sh.ffmpeg(*args)
36
- except sh.ErrorReturnCode as e:
37
- raise RuntimeError(
38
- f"Command {e.full_cmd} exited with {e.exit_code}\n\n{e.stderr.decode()}"
39
- )
40
-
41
-
42
- def milliseconds_to_duration(milliseconds):
43
- minutes, rem = divmod(milliseconds / 1000.0, 60)
44
- seconds, ms = divmod(1000 * rem, 1000)
45
- return f"00:{int(minutes):02d}:{int(seconds):02d}.{int(ms)}"
@@ -1,72 +0,0 @@
1
- import logging
2
- import os
3
- import shutil
4
- from pathlib import Path
5
-
6
- import boto3
7
- from botocore.exceptions import ClientError
8
- from google.cloud import storage
9
- from google.cloud.exceptions import GoogleCloudError
10
-
11
-
12
- def upload(filename: Path, target: str):
13
- if target.startswith(("gs://", "s3://")):
14
- service, _, target = str(target).partition("://")
15
- parts = target.split("/")
16
- bucket = parts.pop(0)
17
- object_name = "/".join(parts)
18
- if service == "s3":
19
- upload_to_s3(filename, bucket, object_name)
20
- elif service == "gs":
21
- upload_to_storage(filename, bucket, object_name)
22
- else:
23
- logging.error(f"Unknown upload service '{service}'")
24
-
25
- else:
26
- shutil.copy(filename, target)
27
-
28
-
29
- def upload_to_s3(filename, bucket, object_name=None):
30
- """Upload a file to an S3 bucket
31
-
32
- :param file_name: File to upload
33
- :param bucket: Bucket to upload to
34
- :param object_name: S3 object name. If not specified then file_name is used
35
- :return: True if file was uploaded, else False
36
- """
37
- if object_name is None:
38
- object_name = os.path.basename(filename)
39
-
40
- s3_client = boto3.client("s3")
41
-
42
- try:
43
- s3_client.upload_file(filename, bucket, object_name)
44
- except ClientError as e:
45
- logging.error(e)
46
- return False
47
-
48
- return True
49
-
50
-
51
- def upload_to_storage(filename, bucket, object_name=None):
52
- """Upload a file to a Cloud Storage bucket
53
-
54
- :param file_name: File to upload
55
- :param bucket: Bucket to upload to
56
- :param object_name: object name. If not specified then file_name is used
57
- :return: True if file was uploaded, else False
58
- """
59
- if object_name is None:
60
- object_name = os.path.basename(filename)
61
-
62
- client = storage.Client()
63
- bucket = client.bucket(bucket)
64
- blob = bucket.blob(object_name)
65
-
66
- try:
67
- blob.upload_from_filename(filename)
68
- except GoogleCloudError as e:
69
- logging.error(e)
70
- return False
71
-
72
- return True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes