Skip to content

Commit 0f0b4e8

Browse files
committed
Merge branch 'feature/unoserver-celery' into feature/buff-worms
Closes #391 [ENG-7610]
2 parents a080e58 + 7b643df commit 0f0b4e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+373
-134
lines changed

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
# Add any Sphinx extension module names here, as strings. They can be extensions
3232
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
33-
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest']
33+
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'stevedore.sphinxext']
3434

3535
# Add any paths that contain templates here, relative to this directory.
3636
templates_path = ['_templates']

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Guide
2626
overview
2727
integrations
2828
code
29+
plugins
2930

3031
Project info
3132
------------

docs/plugins.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
Plugins
3+
-------
4+
5+
Auth Providers
6+
==============
7+
8+
.. list-plugins:: mfr.providers
9+
:detailed:
10+
11+
Exporters
12+
=========
13+
14+
.. list-plugins:: mfr.exporters
15+
16+
Renderers
17+
=========
18+
19+
.. list-plugins:: mfr.renderers
20+

mfr/core/extension.py

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import abc
2+
import asyncio
3+
import time
4+
from dataclasses import dataclass, field
25

6+
from waterbutler.core.streams import StringStream
7+
from waterbutler.core.utils import make_provider
8+
9+
from mfr.server import settings
310
from mfr.core.metrics import MetricsRecord
11+
from mfr.core.provider import ProviderMetadata
12+
from mfr.tasks.serializer import serializable
413

514

615
class BaseExporter(metaclass=abc.ABCMeta):
@@ -42,19 +51,28 @@ def _get_module_name(self):
4251
.replace('mfr.extensions.', '', 1) \
4352
.replace('.export', '', 1)
4453

45-
54+
@serializable
55+
@dataclass
4656
class BaseRenderer(metaclass=abc.ABCMeta):
47-
48-
def __init__(self, metadata, file_path, url, assets_url, export_url):
49-
self.metadata = metadata
50-
self.file_path = file_path
51-
self.url = url
52-
self.assets_url = f'{assets_url}/{self._get_module_name()}'
53-
self.export_url = export_url
54-
self.renderer_metrics = MetricsRecord('renderer')
57+
metadata: ProviderMetadata
58+
file_path: str
59+
url: str
60+
assets_url: str
61+
export_url: str
62+
renderer_metrics: MetricsRecord = field(default=None)
63+
metrics: MetricsRecord = field(default=None)
64+
65+
def __post_init__(self):
66+
self.assets_url = f'{self.assets_url}/{self._get_module_name()}'
67+
self.renderer_metrics = MetricsRecord('renderer',)
5568
if self._get_module_name():
5669
self.metrics = self.renderer_metrics.new_subrecord(self._get_module_name())
5770

71+
if name := self.metadata.name:
72+
self.cache_file_path_str = f'/export/{self.metadata.unique_key}.{name}'
73+
else:
74+
self.cache_file_path_str = f'/export/{self.metadata.unique_key}'
75+
5876
self.renderer_metrics.merge({
5977
'class': self._get_module_name(),
6078
'ext': self.metadata.ext,
@@ -73,10 +91,55 @@ def __init__(self, metadata, file_path, url, assets_url, export_url):
7391
except AttributeError:
7492
pass
7593

94+
@property
95+
def cache_provider(self):
96+
return make_provider(
97+
settings.CACHE_PROVIDER_NAME,
98+
{}, # User information which can be left blank
99+
settings.CACHE_PROVIDER_CREDENTIALS,
100+
settings.CACHE_PROVIDER_SETTINGS
101+
)
102+
103+
async def get_cache_file_path(self):
104+
return await self.cache_provider.validate_path(self.cache_file_path_str)
105+
76106
@abc.abstractmethod
77-
def render(self):
107+
def _render(self) -> str:
78108
pass
79109

110+
async def render(self):
111+
if self.use_celery or self.cache_result:
112+
self.cache_file_path = await self.cache_provider.validate_path(self.cache_file_path_str)
113+
if not self.use_celery:
114+
rendition = await self.do_render()
115+
return StringStream(rendition)
116+
else:
117+
from mfr.tasks.render import render
118+
result = render.delay(self)
119+
for i in range(100 * 60 * 10):
120+
if not result.ready():
121+
time.sleep(0.01)
122+
else:
123+
return await self.cache_provider.download(self.cache_file_path)
124+
125+
return None
126+
127+
async def do_render(self):
128+
if self.use_celery or self.cache_result:
129+
file_path_task = asyncio.ensure_future(self.get_cache_file_path())
130+
rendition = await asyncio.get_running_loop().run_in_executor(None, self._render)
131+
if self.use_celery or self.cache_result:
132+
upload_task = asyncio.ensure_future(
133+
self.cache_provider.upload(
134+
StringStream(rendition),
135+
await file_path_task
136+
)
137+
)
138+
if self.use_celery:
139+
await upload_task
140+
return rendition
141+
142+
@property
80143
@abc.abstractmethod
81144
def file_required(self):
82145
"""Does the rendering html need the raw file content to display correctly?
@@ -85,10 +148,15 @@ def file_required(self):
85148
"""
86149
pass
87150

151+
@property
88152
@abc.abstractmethod
89-
def cache_result(self):
153+
def cache_result(self) -> bool:
90154
pass
91155

156+
@property
157+
def use_celery(self) -> bool:
158+
return False
159+
92160
def _get_module_name(self):
93161
return self.__module__ \
94162
.replace('mfr.extensions.', '', 1) \

mfr/core/metrics.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import copy
2+
from dataclasses import dataclass, field
3+
4+
from mfr.tasks.serializer import serializable
25

36

47
def _merge_dicts(a, b, path=None):
@@ -93,16 +96,17 @@ def _set_dotted_key(store, key, value):
9396
current = current[part]
9497
current[parts[-1]] = value
9598

96-
99+
@serializable
100+
@dataclass
97101
class MetricsRecord(MetricsBase):
98102
"""An extension to MetricsBase that carries a category and list of submetrics. When
99103
serialized, will include the serialized child metrics
100104
"""
105+
category: str
106+
subrecords: list = field(default_factory=list)
101107

102-
def __init__(self, category):
108+
def __post_init__(self):
103109
super().__init__()
104-
self.category = category
105-
self.subrecords = []
106110

107111
@property
108112
def key(self):
@@ -121,20 +125,23 @@ def serialize(self):
121125
def new_subrecord(self, name):
122126
"""Create a new MetricsSubRecord object with our category and save it to the subrecords
123127
list."""
124-
subrecord = MetricsSubRecord(self.category, name)
128+
subrecord = MetricsSubRecord(category=self.category, name=name)
125129
self.subrecords.append(subrecord)
126130
return subrecord
127131

128-
132+
@serializable
133+
@dataclass
129134
class MetricsSubRecord(MetricsRecord):
130135
"""An extension to MetricsRecord that carries a name in addition to a category. Will identify
131136
itself as {category}_{name}. Can create its own subrecord whose category will be this
132137
subrecord's ``name``.
133138
"""
139+
name: str = field(default=None)
134140

135-
def __init__(self, category, name):
136-
super().__init__(category)
137-
self.name = name
141+
def __post_init__(self):
142+
super().__post_init__()
143+
if self.name is None:
144+
raise TypeError('name must be provided')
138145

139146
@property
140147
def key(self):
@@ -153,6 +160,6 @@ def new_subrecord(self, name):
153160
print(child.key) # foo_bar
154161
print(grandchild.key) # bar_baz
155162
"""
156-
subrecord = MetricsSubRecord(self.name, name)
163+
subrecord = MetricsSubRecord(category=self.name, name=name)
157164
self.subrecords.append(subrecord)
158165
return subrecord

mfr/core/provider.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import abc
2+
from dataclasses import dataclass
3+
24
import markupsafe
35

46
import furl
57

68
from mfr.core import exceptions
79
from mfr.server import settings
810
from mfr.core.metrics import MetricsRecord
11+
from mfr.tasks.serializer import serializable
912

1013

1114
class BaseProvider(metaclass=abc.ABCMeta):
@@ -47,16 +50,15 @@ def metadata(self):
4750
def download(self):
4851
pass
4952

50-
53+
@serializable
54+
@dataclass
5155
class ProviderMetadata:
52-
53-
def __init__(self, name, ext, content_type, unique_key, download_url, stable_id=None):
54-
self.name = name
55-
self.ext = ext
56-
self.content_type = content_type
57-
self.unique_key = unique_key
58-
self.download_url = download_url
59-
self.stable_id = stable_id
56+
name: str
57+
ext: str
58+
content_type: str
59+
unique_key: str
60+
download_url: str
61+
stable_id: str = None
6062

6163
def serialize(self):
6264
return {

mfr/core/utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,13 @@ def make_renderer(name, metadata, file_path, url, assets_url, export_url):
100100
namespace='mfr.renderers',
101101
name=normalized_name,
102102
invoke_on_load=True,
103-
invoke_args=(metadata, file_path, url, assets_url, export_url),
103+
invoke_kwds={
104+
'metadata': metadata,
105+
'file_path': file_path,
106+
'url': url,
107+
'assets_url': assets_url,
108+
'export_url': export_url
109+
},
104110
).driver
105111
except RuntimeError:
106112
raise exceptions.MakeRendererError(

mfr/extensions/audio/render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class AudioRenderer(extension.BaseRenderer):
1313
os.path.join(os.path.dirname(__file__), 'templates')
1414
]).get_template('viewer.mako')
1515

16-
def render(self):
16+
def _render(self):
1717
safe_url = escape_url_for_template(self.url)
1818
return self.TEMPLATE.render(base=self.assets_url, url=safe_url)
1919

mfr/extensions/codepygments/render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def __init__(self, *args, **kwargs):
3030
super().__init__(*args, **kwargs)
3131
self.metrics.add('pygments_version', pygments.__version__)
3232

33-
def render(self):
33+
def _render(self):
3434
file_size = os.path.getsize(self.file_path)
3535
if file_size > settings.MAX_SIZE:
3636
raise exceptions.FileTooLargeError(

mfr/extensions/image/render.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ImageRenderer(extension.BaseRenderer):
1515
os.path.join(os.path.dirname(__file__), 'templates')
1616
]).get_template('viewer.mako')
1717

18-
def render(self):
18+
def _render(self):
1919
self.metrics.add('needs_export', False)
2020
if self.metadata.ext in settings.EXPORT_EXCLUSIONS:
2121
download_url = munge_url_for_localdev(self.url)

0 commit comments

Comments
 (0)