├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── PacletInfo.m
├── README.md
├── docs
└── assets
│ └── image1.png
├── run.py
├── setup.cfg
├── setup.py
└── wolframwebengine
├── __init__.py
├── __main__.py
├── cli
├── __init__.py
├── commands
│ ├── __init__.py
│ ├── benchmark_server.py
│ ├── refactor.py
│ ├── runserver.py
│ └── test.py
└── dispatch.py
├── examples
├── __init__.py
├── aiohttp_application.py
├── demo
│ ├── ask.wl
│ ├── ca.wl
│ ├── form.wl
│ └── trip.wl
├── djangoapp
│ ├── __init__.py
│ ├── manage.py
│ └── urls.py
└── sampleapp
│ ├── api
│ └── index.wl
│ ├── foo
│ ├── bar
│ │ ├── index.wl
│ │ └── something.wl
│ ├── index.wl
│ └── something.wl
│ ├── form
│ └── index.wl
│ ├── index.wl
│ ├── random.wl
│ ├── some.json
│ ├── some.m
│ ├── some.mx
│ ├── some.wl
│ └── some.wxf
├── server
├── __init__.py
├── app.py
└── explorer.py
├── tests
├── __init__.py
├── application_aiohttp.py
├── application_django.py
└── folder_explorer.py
└── web
├── __init__.py
├── aiohttp.py
├── django.py
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | *.pyc
3 | *$py.class
4 | /build
5 | /dist
6 | *.egg-info
7 | wolframclient/tests/local_config.json
8 | /htmlcov
9 | .coverage
10 | .vscode
11 | *.code-workspace
12 | .DS_Store
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Version 1.0.4
2 | - fix for https://github.com/WolframResearch/WolframWebEngineForPython/issues/10
3 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Wolfram®
2 |
3 | Thank you for taking the time to contribute to the [Wolfram Research](https://github.com/wolframresearch) repos on GitHub.
4 |
5 | ## Licensing of Contributions
6 |
7 | By contributing to Wolfram, you agree and affirm that:
8 |
9 | > Wolfram may release your contribution under the terms of the [MIT license](https://opensource.org/licenses/MIT); and
10 |
11 | > You have read and agreed to the [Developer Certificate of Origin](http://developercertificate.org/), version 1.1 or later.
12 |
13 | Please see [LICENSE](LICENSE) for licensing conditions pertaining
14 | to individual repositories.
15 |
16 |
17 | ## Bug reports
18 |
19 | ### Security Bugs
20 |
21 | Please **DO NOT** file a public issue regarding a security issue.
22 | Rather, send your report privately to security@wolfram.com. Security
23 | reports are appreciated and we will credit you for it. We do not offer
24 | a security bounty, but the forecast in your neighborhood will be cloudy
25 | with a chance of Wolfram schwag!
26 |
27 | ### General Bugs
28 |
29 | Please use the repository issues page to submit general bug issues.
30 |
31 | Please do not duplicate issues.
32 |
33 | Please do send a complete and well-written report to us. Note: **the
34 | thoroughness of your report will positively correlate to our willingness
35 | and ability to address it**.
36 |
37 | When reporting issues, always include:
38 |
39 | * Your version of *Mathematica*® or the Wolfram Language™.
40 | * Your operating system.
41 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM wolframresearch/wolframengine:latest
2 |
3 | USER root
4 |
5 | RUN apt-get update -y && \
6 | apt-get install -y python3 python3-pip && \
7 | python3 -m pip install wolframclient
8 |
9 | COPY . /tmp/build
10 | RUN pip3 install /tmp/build && \
11 | rm -r /tmp/build && \
12 | chown -R wolframengine /srv
13 |
14 | USER wolframengine
15 | EXPOSE 18000
16 |
17 | ENTRYPOINT ["/usr/bin/python3", "-m", "wolframwebengine", "--domain", "0.0.0.0"]
18 | CMD ["/srv"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2018 Wolfram Research Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE PacletInfo.m run.py
2 | recursive-include wolframwebengine *.m
3 | recursive-include wolframwebengine *.wl
4 | recursive-include wolframwebengine *.wxf
5 | recursive-include wolframwebengine *.mx
6 | recursive-include wolframwebengine *.json
7 | recursive-include wolframwebengine/tests *.py
--------------------------------------------------------------------------------
/PacletInfo.m:
--------------------------------------------------------------------------------
1 | Paclet[
2 | Name -> "WolframEngineForPython",
3 | Version -> "1.0.0",
4 | MathematicaVersion -> "12.0+",
5 | Loading -> Automatic,
6 | Extensions -> {}
7 | ]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wolfram Web Engine for Python
2 |
3 | Wolfram Web Engine for Python uses the Python AIOHTTP web server to handle requests for a Wolfram Engine.
4 | Web pages are specified on the server with standard Wolfram Language functions such as [APIFunction](https://reference.wolfram.com/language/ref/APIFunction.html), [FormFunction](https://reference.wolfram.com/language/ref/FormFunction.html), [FormPage](https://reference.wolfram.com/language/ref/FormPage.html),
5 | [URLDispatcher](https://reference.wolfram.com/language/ref/URLDispatcher.html), [AskFunction](https://reference.wolfram.com/language/ref/AskFunction.html), [HTTPResponse](https://reference.wolfram.com/language/ref/HTTPResponse.html), [HTTPRedirect](https://reference.wolfram.com/language/ref/HTTPRedirect.html), etc. This allows you to integrate Wolfram Language
6 | functionality seamlessly with existing Python web applications like Django and AIOHTTP.
7 |
8 | ## Getting Started
9 |
10 | ### Prerequisites
11 |
12 | 1. Python 3.5 or higher
13 | 2. Wolfram Language 11.3 or higher (Mathematica, Wolfram Desktop, or Wolfram Engine)
14 | 3. [WolframClientForPython](!https://github.com/WolframResearch/WolframClientForPython)
15 |
16 | ### Install Using pip (Recommended)
17 | Recommended for most users. It installs the latest stable version released by Wolfram Research.
18 |
19 | Evaluate the following command in a terminal:
20 |
21 | ```
22 | >>> pip3 install wolframwebengine
23 | ```
24 |
25 | ### Install Using Git
26 |
27 | Recommended for developers who want to install the library along with the full source code.
28 | Clone the library’s repository:
29 |
30 | ```
31 | >>> git clone git://github.com/WolframResearch/WolframWebEngineForPython
32 | ```
33 |
34 | Install the library in your site-package directory:
35 |
36 | ```
37 | >>> cd WolframWebEngineForPython
38 | >>> pip3 install .
39 | ```
40 |
41 | The following method is not installing the library globally, therefore all the example commands needs to run from the cloned directory.
42 |
43 | ### Start a demo server
44 |
45 | Start a demo server by doing:
46 |
47 | ```
48 | python3 -m wolframwebengine --demo
49 | ----------------------------------------------------------------------
50 | Address http://localhost:18000/
51 | Folder /Users/rdv/Desktop/wolframengineforpython/wolframwebengine/examples/demoapp
52 | Index index.wl
53 | ----------------------------------------------------------------------
54 | (Press CTRL+C to quit)
55 | ```
56 |
57 | Now you can open your web browser at the address http://localhost:18000/
58 |
59 | 
60 |
61 | ## Two different ways of structuring an application:
62 |
63 | 1. Use a single file with URLDispatcher
64 | 2. Use multiple files in a directory layout
65 |
66 | ### Single file with URLDispatcher
67 |
68 | One way to run your server is to direct all requests to a single file
69 | that runs a Wolfram Language [URLDispatcher](https://reference.wolfram.com/language/ref/URLDispatcher.html) function.
70 |
71 | Write the following content in a file called `dispatcher.m`:
72 |
73 | ```
74 | URLDispatcher[{
75 | "/api" -> APIFunction["x" -> "String"],
76 | "/form" -> FormFunction["x" -> "String"],
77 | "/" -> "hello world!"
78 | }]
79 | ```
80 |
81 | From the same location run:
82 |
83 | ```
84 | >>> python3 -m wolframwebengine dispatcher.m
85 | ----------------------------------------------------------------------
86 | Address http://localhost:18000/
87 | File /Users/rdv/Desktop/dispatcher.m
88 | ----------------------------------------------------------------------
89 | (Press CTRL+C to quit)
90 | ```
91 |
92 | All incoming requests will now be routed to the `URLDispatcher` function in `dispatcher.m`.
93 | You can now open the following urls in your browser:
94 |
95 | ```
96 | http://localhost:18000/
97 | http://localhost:18000/form
98 | http://localhost:18000/api
99 | ```
100 |
101 | For more information about `URLDispatcher` please refer to the [online documentation](https://reference.wolfram.com/language/ref/URLDispatcher.html).
102 |
103 | ### Multiple files in a directory layout
104 |
105 | Another way to write an application is to create a directory structure that is served by the server. The url for each file will match the file's directory path.
106 |
107 | The server will serve content with the following rules:
108 |
109 | 1. All files with extensions '.m', '.mx', '.wxf', '.wl' will be evaluated in the Kernel using [GenerateHTTPResponse](https://reference.wolfram.com/language/ref/GenerateHTTPResponse.html) on the content of the file.
110 | 2. Any other file will be served as static content.
111 | 3. If the request path corresponds to a directory on disk, the server will search for a file named index.wl in that directory. This convention can be changed with the --index option.
112 |
113 | Create an application by running the following code in your current location:
114 |
115 | ```
116 | mkdir testapp
117 | mkdir testapp/form
118 | mkdir testapp/api
119 | echo 'ExportForm[{"hello", UnixTime[]}, "JSON"]' > testapp/index.wl
120 | echo 'FormFunction["x" -> "String"]' > testapp/form/index.wl
121 | echo 'APIFunction["x" -> "Number", #x! &]' > testapp/api/index.wl
122 | echo 'HTTPResponse["hello world"]' > testapp/response.wl
123 | echo '["some", "static", "JSON"]' > testapp/static.json
124 | ```
125 |
126 | Start the application by running:
127 |
128 | ```
129 | >>> python3 -m wolframwebengine testapp
130 | ----------------------------------------------------------------------
131 | Address http://localhost:18000/
132 | Folder /Users/rdv/Desktop/testapp
133 | Index index.wl
134 | ----------------------------------------------------------------------
135 | (Press CTRL+C to quit)
136 | ```
137 |
138 | Then open the browser at the following locations:
139 |
140 | ```
141 | http://localhost:18000/
142 | http://localhost:18000/form
143 | http://localhost:18000/api?x=4
144 | http://localhost:18000/response.wl
145 | http://localhost:18000/static.json
146 | ```
147 |
148 | One advantage of a multi-file application structure is that is very easy to extend the application. You can simply place new files into the appropriate location in your application directory and they will automatically be served.
149 |
150 |
151 | ## Using Docker
152 |
153 | Wolfram Web Engine for Python is available as [a container image from Docker Hub](https://hub.docker.com/r/wolframresearch/wolframwebengineforpython) for use in containerized environments.
154 |
155 | This image is based on the [official Wolfram Engine Docker image](https://hub.docker.com/r/wolframresearch/wolframengine); information on product activation and license terms
156 | is available on the [Docker Hub page](https://hub.docker.com/r/wolframresearch/wolframengine) for the latter image.
157 |
158 | ```
159 | # exposes the server on port 8080 of the host machine
160 | >>> docker run -ti -p 8080:18000 wolframresearch/wolframwebengineforpython --demo
161 |
162 | # serve files from the /srv directory
163 | >>> docker run -ti -p 8080:18000 wolframresearch/wolframwebengineforpython /srv
164 | ```
165 |
166 | The commands above do not include activation/licensing configuration; see the [official Wolfram Engine Docker image](https://hub.docker.com/r/wolframresearch/wolframengine) for information on activating the Wolfram Engine kernel.
167 |
168 |
169 | Note regarding on-demand licensing: As Wolfram Web Engine for Python does not use WolframScript, the `-entitlement` command-line option and the `WOLFRAMSCRIPT_ENTITLEMENTID`
170 | environment variable cannot be used to pass an on-demand license entitlement ID to the Wolfram Engine kernel inside this image.
171 | As a workaround, the `WOLFRAMINIT` environment variable can be set to pass both the entitlement ID and the license server address to the kernel:
172 |
173 | ```
174 | >>> docker run -ti -p 8080:18000 --env WOLFRAMINIT='-pwfile !cloudlm.wolfram.com -entitlement O-WSTD-DA42-GKX4Z6NR2DSZR' wolframresearch/wolframwebengineforpython --demo
175 | ```
176 |
177 |
178 | ## Options
179 |
180 | ```
181 | >>> python3 -m wolframwebengine --help
182 | usage: __main__.py [-h] [--port PORT] [--domain DOMAIN] [--kernel KERNEL]
183 | [--poolsize POOLSIZE] [--cached] [--lazy] [--index INDEX]
184 | [--demo [{None,ask,trip,ca,form}]]
185 | [path]
186 |
187 | positional arguments:
188 | path
189 |
190 | optional arguments:
191 | -h, --help show this help message and exit
192 | --port PORT Insert the port.
193 | --domain DOMAIN Insert the domain.
194 | --kernel KERNEL Insert the kernel path.
195 | --poolsize POOLSIZE Insert the kernel pool size.
196 | --startuptimeout SECONDS
197 | Startup timeout (in seconds) for kernels in the pool.
198 | --cached The server will cache the WL input expression.
199 | --lazy The server will start the kernels on the first
200 | request.
201 | --index INDEX The file name to search for folder index.
202 | --demo [{None,ask,trip,ca,form}]
203 | Run a demo application
204 | ```
205 |
206 | #### demo
207 |
208 | Run a demo application:
209 |
210 | 1. __ask__: Marginal Tax rate calculator using AskFunction.
211 | 2. __trip__: Trip calculator using FormFunction and TravelDirections.
212 | 3. __ca__: Cellular Automaton demo gallery using URLDispatcher and GalleryView.
213 | 4. __form__: ImageProcessing demo using FormFunction.
214 |
215 | ```
216 | >>> python3 -m wolframwebengine --demo ca
217 | ----------------------------------------------------------------------
218 | Address http://localhost:18000/
219 | File /Users/rdv/Wolfram/git/wolframengineforpython/wolframwebengine/examples/demo/ca.wl
220 | ----------------------------------------------------------------------
221 | (Press CTRL+C to quit)
222 | ```
223 |
224 | #### path
225 |
226 | The first argument can be a folder or a single file.
227 |
228 | Write a file on your current folder:
229 |
230 | ```
231 | >>> mkdir testapp
232 | >>> echo 'ExportForm[{"hello", "from", "Kernel", UnixTime[]}, "JSON"]' > testapp/index.wl
233 | ```
234 |
235 | Then from a command line run:
236 |
237 | ```
238 | >>> python3 -m wolframwebengine testapp
239 | ----------------------------------------------------------------------
240 | Address http://localhost:18000/
241 | Folder /Users/rdv/Desktop/testapp
242 | Index index.wl
243 | ----------------------------------------------------------------------
244 | (Press CTRL+C to quit)
245 | ```
246 |
247 | If the first argument is a file, requests will be redirected to files in that directory if the url extension is '.m', '.mx', '.wxf', '.wl'. If the extension cannot be handled by a kernel, the file will be served as static content.
248 |
249 | If the request path is a folder the server will search for an index.wl in the same folder.
250 |
251 | #### --index
252 |
253 | Specify the default file name for the folder index.
254 | Defaults to index.wl
255 |
256 | ```
257 | python3 -m wolframwebengine --index index.wxf
258 | ----------------------------------------------------------------------
259 | Address http://localhost:18000/
260 | Folder /Users/rdv/Desktop
261 | Index index.wxf
262 | ----------------------------------------------------------------------
263 | (Press CTRL+C to quit)
264 | ```
265 |
266 |
267 | #### --cached
268 |
269 | If --cached is present the code in each file will be run only once, with subsequent requests retrieving the cached result.
270 |
271 | ```
272 | >>> python3 -m wolframwebengine --cached
273 | ----------------------------------------------------------------------
274 | Address http://localhost:18000/
275 | Folder /Users/rdv/Desktop
276 | Index index.wl
277 | ----------------------------------------------------------------------
278 | (Press CTRL+C to quit)
279 | ```
280 |
281 | Visit the browser and refresh the page.
282 |
283 |
284 | #### --port PORT
285 |
286 | Allows you to specify the PORT of the webserver. Defaults to 18000.
287 |
288 | ```
289 | >>> python3 -m wolframwebengine --port 9090
290 | ----------------------------------------------------------------------
291 | Address http://localhost:9090/
292 | Folder /Users/rdv/Desktop
293 | Index index.wl
294 | ----------------------------------------------------------------------
295 | (Press CTRL+C to quit)
296 | ```
297 |
298 | #### --domain DOMAIN
299 |
300 | Allows you to specify the DOMAIN of the webserver. By default the webserver only listens to localhost, use `0.0.0.0` to listen on all network interfaces.
301 |
302 | ```
303 | >>> python3 -m wolframwebengine --domain 0.0.0.0
304 | ----------------------------------------------------------------------
305 | Address http://0.0.0.0:18000/
306 | Folder /Users/rdv/Desktop
307 | Index index.wl
308 | ----------------------------------------------------------------------
309 | (Press CTRL+C to quit)
310 | ```
311 |
312 | #### --initfile FILE
313 |
314 | Allows you to specify a custom file containing code to be run when a new kernel is started
315 |
316 | ```
317 | >>> python3 -m wolframwebengine --initfile myinit.m
318 | ----------------------------------------------------------------------
319 | Address http://localhost:18000/
320 | Folder /Users/rdv/Desktop
321 | Index index.wl
322 | ----------------------------------------------------------------------
323 | (Press CTRL+C to quit)
324 | ```
325 |
326 | #### --kernel KERNEL
327 |
328 | Allows you to specify the Kernel path
329 |
330 | ```
331 | >>> python3 -m wolframwebengine --kernel '/Applications/Wolfram Desktop 12.app/Contents/MacOS/WolframKernel'
332 | ----------------------------------------------------------------------
333 | Address http://localhost:18000/
334 | Folder /Users/rdv/Desktop
335 | Index index.wl
336 | ----------------------------------------------------------------------
337 | (Press CTRL+C to quit)
338 | ```
339 |
340 | #### --poolsize SIZE
341 |
342 | Wolfram Web Engine for Python will launch a pool of Wolfram Language kernels to handle incoming requests. Running more than one kernel can improve responsiveness if multiple requests arrive at the same time. The --poolsize option lets you change the number of kernels that will be launched. Defaults to 1.
343 | ```
344 | >>> python3 -m wolframwebengine --poolsize 4
345 | ----------------------------------------------------------------------
346 | Address http://localhost:18000/
347 | Folder /Users/rdv/Desktop
348 | Index index.wl
349 | ----------------------------------------------------------------------
350 | (Press CTRL+C to quit)
351 | ```
352 |
353 | #### --startuptimeout SECONDS
354 |
355 | By default, an attempt to start a kernel will be aborted if the kernel is not ready after 20 seconds. If your application contains long-running initialization code, you may need to raise this timeout.
356 | ```
357 | >>> python3 -m wolframwebengine
358 | (...)
359 | Kernel process started with PID: 485
360 | Socket exception: Failed to read any message from socket tcp://127.0.0.1:5106 after 20.0 seconds and 245 retries.
361 | Failed to start.
362 |
363 |
364 | >>> python3 -m wolframwebengine --startuptimeout 50
365 | (...)
366 | Kernel process started with PID: 511
367 | Connected to logging socket: tcp://127.0.0.1:5447
368 | Kernel 511 is ready. Startup took 35.43 seconds.
369 | ```
370 |
371 | #### --lazy
372 |
373 | If the option is present the server will wait for the first request to spawn the kernels, instead of spawning them immediately.
374 |
375 |
376 | #### --client_max_size MB
377 |
378 | The maximum amount of megabytes allowed for file upload. Defaults to 10.
379 | ```
380 | >>> python3 -m wolframwebengine --client_max_size 150
381 | ----------------------------------------------------------------------
382 | Address http://localhost:18000/
383 | Folder /Users/rdv/Desktop
384 | Index index.wl
385 | ----------------------------------------------------------------------
386 | (Press CTRL+C to quit)
387 | ```
388 |
389 | ## Integrating an existing application
390 |
391 | Wolfram Web Engine for Python can be used to augment an existing python application instead of creating a new one.
392 | We currently support the following frameworks:
393 |
394 | ### Django
395 |
396 | If you have an existing [Django](!https://www.djangoproject.com/) application you can use the `django_wl_view` decorator to evaluate Wolfram Language code during a web request.
397 |
398 | ```python
399 | from __future__ import absolute_import, print_function, unicode_literals
400 |
401 | from django.http import HttpResponse
402 | from django.urls import path
403 |
404 | from wolframclient.language import wl
405 | from wolframclient.evaluation import WolframLanguageSession
406 | from wolframwebengine.web import django_wl_view
407 |
408 | session = WolframLanguageSession()
409 |
410 | def django_view(request):
411 | return HttpResponse("hello from django")
412 |
413 | @django_wl_view(session)
414 | def form_view(request):
415 | return wl.FormFunction({"x": "String"}, wl.Identity, "JSON")
416 |
417 |
418 | @django_wl_view(session)
419 | def api_view(request):
420 | return wl.APIFunction({"x": "String"}, wl.Identity, "JSON")
421 |
422 |
423 | urlpatterns = [
424 | path("", django_view, name="home"),
425 | path("form", form_view, name="form"),
426 | path("api", api_view, name="api"),
427 | ]
428 | ```
429 |
430 | The decorator can be used with any kind of synchronous evaluator exposed and documented in [WolframClientForPython](!https://github.com/WolframResearch/WolframClientForPython).
431 |
432 | ### Aiohttp
433 |
434 | If you have an existing [Aiohttp](!https://docs.aiohttp.org/en/stable/web_reference.html) server running you can use the `aiohttp_wl_view` decorator to evaluate Wolfram Language code during a web request.
435 |
436 | ```python
437 | from aiohttp import web
438 |
439 | from wolframclient.evaluation import WolframEvaluatorPool
440 | from wolframclient.language import wl
441 | from wolframwebengine.web import aiohttp_wl_view
442 |
443 | session = WolframEvaluatorPool(poolsize=4)
444 | routes = web.RouteTableDef()
445 |
446 |
447 | @routes.get("/")
448 | async def hello(request):
449 | return web.Response(text="Hello from aiohttp")
450 |
451 |
452 | @routes.get("/form")
453 | @aiohttp_wl_view(session)
454 | async def form_view(request):
455 | return wl.FormFunction(
456 | {"x": "String"}, wl.Identity, AppearanceRules={"Title": "Hello from WL!"}
457 | )
458 |
459 |
460 | @routes.get("/api")
461 | @aiohttp_wl_view(session)
462 | async def api_view(request):
463 | return wl.APIFunction({"x": "String"}, wl.Identity)
464 |
465 |
466 | @routes.get("/app")
467 | @aiohttp_wl_view(session)
468 | async def app_view(request):
469 | return wl.Once(wl.Get("path/to/my/complex/wl/app.wl"))
470 |
471 |
472 | app = web.Application()
473 | app.add_routes(routes)
474 |
475 | if __name__ == "__main__":
476 | web.run_app(app)
477 | ```
478 |
479 | The decorator can be used with any kind of asynchronous evaluator exposed and documented in [WolframClientForPython](!https://github.com/WolframResearch/WolframClientForPython).
480 |
481 |
--------------------------------------------------------------------------------
/docs/assets/image1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/d1bdd91d8b754d1996ed8ca1949a2fa889344931/docs/assets/image1.png
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from __future__ import absolute_import, print_function, unicode_literals
4 |
5 | if __name__ == '__main__':
6 |
7 | #this will perform an auto install of missing modules using PIP
8 | #this should not be used in production, but it's handy when we are giving this paclet to other developers
9 | #as it provides convenient access to unit tests, profiler, and benchmarking.
10 |
11 | from wolframwebengine.cli.dispatch import execute_from_command_line
12 |
13 | execute_from_command_line()
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal = 1
3 |
4 | [metadata]
5 | license_file = LICENSE
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | from __future__ import absolute_import, print_function, unicode_literals
4 |
5 | import sys
6 |
7 | try:
8 | from setuptools import setup, find_packages
9 | except ImportError:
10 | sys.stderr.write("""Could not import setuptools or your version
11 | of the package is out of date.
12 |
13 | Make sure you have pip and setuptools installed and upgraded and try again:
14 | $ python -m pip install --upgrade pip setuptools
15 | $ python setup.py install
16 |
17 | """)
18 |
19 | def load_tests():
20 | from wolframwebengine.cli.commands.test import Command as TestCommand
21 | TestCommand().handle()
22 |
23 | setup(
24 | name = 'wolframwebengine',
25 | version = '1.0.4',
26 | description = 'A Python library with various tools to start a wolfram engine a server content.',
27 | keywords=['Wolfram Language', 'Wolfram Desktop', 'Mathematica', 'Web Development', 'Wolfram Web Engine'],
28 | author = 'Wolfram Research, Riccardo Di Virgilio',
29 | author_email = 'support@wolfram.com, riccardod@wolfram.com',
30 | packages=find_packages(),
31 | test_suite='setup.load_tests',
32 | python_requires='>=3.5.3',
33 | include_package_data=True,
34 | install_requires = [
35 | 'wolframclient>=1.1.10',
36 | 'aiohttp>=3.5.4'
37 | ],
38 | project_urls={
39 | 'Source code': 'https://github.com/WolframResearch/WolframWebEngineForPython',
40 | 'Wolfram Research': 'https://www.wolfram.com',
41 | },
42 | classifiers = [
43 | "License :: OSI Approved :: MIT License",
44 | "Programming Language :: Python",
45 | "Programming Language :: Python :: 3",
46 | "Programming Language :: Python :: 3.5",
47 | "Programming Language :: Python :: 3.6",
48 | "Programming Language :: Python :: 3.7",
49 | "Topic :: Software Development :: Libraries"
50 | ],
51 | entry_points={
52 | 'console_scripts': [
53 | 'wolframwebengine = wolframwebengine.__main__:main',
54 | ]
55 | }
56 | )
57 |
--------------------------------------------------------------------------------
/wolframwebengine/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/d1bdd91d8b754d1996ed8ca1949a2fa889344931/wolframwebengine/__init__.py
--------------------------------------------------------------------------------
/wolframwebengine/__main__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from wolframwebengine.cli.commands.runserver import Command
4 |
5 |
6 | def main():
7 | Command().main()
8 |
9 |
10 | if __name__ == "__main__":
11 | main()
12 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/commands/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/commands/benchmark_server.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import time
4 | from operator import itemgetter
5 |
6 | from wolframclient.cli.utils import SimpleCommand
7 | from wolframclient.serializers import export
8 | from wolframclient.utils.api import aiohttp, asyncio
9 | from wolframclient.utils.asyncio import run_in_loop
10 | from wolframclient.utils.encoding import force_text
11 | from wolframclient.utils.functional import iterate
12 |
13 |
14 | class Command(SimpleCommand):
15 | """ Run test suites from the tests modules.
16 | A list of patterns can be provided to specify the tests to run.
17 | """
18 |
19 | col_size = 20
20 |
21 | def add_arguments(self, parser):
22 | parser.add_argument(
23 | "--requests",
24 | default=100,
25 | help="Insert the number or request to be done.",
26 | type=int,
27 | )
28 | parser.add_argument(
29 | "--clients", default=4, help="Insert the number of clients.", type=int
30 | )
31 | parser.add_argument(
32 | "--url", default="http://localhost:18000", help="Insert the url to stress."
33 | )
34 | parser.add_argument("--format", default=None)
35 |
36 | async def consumer(self, queue, i):
37 | results = []
38 | async with aiohttp.ClientSession() as session:
39 | while queue:
40 | t1 = time.time()
41 | async with session.get(queue.pop()) as resp:
42 | bytes_count = len(await resp.content.read())
43 | results.append(
44 | {
45 | "time": time.time() - t1,
46 | "bytes": bytes_count,
47 | "success": resp.status == 200,
48 | }
49 | )
50 |
51 | return results
52 |
53 | def generate_tasks(self, requests, clients, url):
54 |
55 | # Create the queue with a fixed size so the producer
56 | # will block until the consumers pull some items out.
57 | queue = [url for i in range(requests)]
58 |
59 | for i in range(clients):
60 | yield self.consumer(queue, i)
61 |
62 | @run_in_loop
63 | async def wait_for_tasks(self, requests, clients, url):
64 | # Wait for all of the coroutines to finish.
65 | results = await asyncio.gather(*self.generate_tasks(requests, clients, url))
66 | results = tuple(iterate(*results))
67 | return results
68 |
69 | def create_data(self, requests, clients, url):
70 |
71 | yield "Url", url
72 | yield "Clients", clients
73 | yield "Requests", requests
74 |
75 | t1 = time.time()
76 |
77 | # Wait for all of the coroutines to finish.
78 | results = self.wait_for_tasks(requests, clients, url)
79 |
80 | s = sum(map(itemgetter("time"), results))
81 | kb = sum(map(itemgetter("bytes"), results)) / 1024
82 | l = len(results)
83 |
84 | t2 = time.time() - t1
85 |
86 | assert l == requests
87 |
88 | yield "Requests OK", sum(map(itemgetter("success"), results))
89 | yield "Requests/sec", 1 / (t2 / l)
90 |
91 | yield "Total time", t2
92 | yield "Avg time", t2 / l
93 |
94 | yield "Total Kb", kb
95 | yield "Avb req Kb", kb / l
96 | yield "Kb/sec", kb / t2
97 |
98 | yield "Client total time", s
99 | yield "Client avg time", s / l
100 |
101 | def table_line(self, *iterable):
102 | self.print(*(force_text(c).ljust(self.col_size) for c in iterable))
103 |
104 | def handle(self, format, **opts):
105 | data = self.create_data(**opts)
106 | if format:
107 | self.print(export(dict(data), target_format=format))
108 | else:
109 | for k, v in data:
110 | if isinstance(v, float):
111 | v = "%.4f" % v
112 | self.table_line(k, v)
113 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/commands/refactor.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from wolframclient.cli.commands.refactor import Command as RefactorCommand
4 |
5 |
6 | class Command(RefactorCommand):
7 |
8 | modules = ["wolframwebengine"]
9 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/commands/runserver.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import logging
4 | import os
5 | import sys
6 |
7 | from aiohttp import web
8 | from aiohttp.abc import AbstractAccessLogger
9 | from wolframclient.cli.utils import SimpleCommand
10 | from wolframclient.exception import WolframKernelException
11 | from wolframclient.utils.api import asyncio
12 | from wolframclient.utils.decorators import cached_property, to_dict
13 | from wolframclient.utils.functional import first
14 | from wolframclient.utils.importutils import module_path
15 |
16 | from wolframwebengine.server.app import create_session, create_view, is_wl_code
17 |
18 |
19 | class AccessLogger(AbstractAccessLogger):
20 | def log(self, request, response, time):
21 | self.logger.info(
22 | "%s %s done in %.4fs: %s" % (request.method, request.path, time, response.status)
23 | )
24 |
25 |
26 | class Command(SimpleCommand):
27 | """ Run test suites from the tests modules.
28 | A list of patterns can be provided to specify the tests to run.
29 | """
30 |
31 | ServerRunner = web.ServerRunner
32 | Server = web.Server
33 | TCPSite = web.TCPSite
34 | AccessLogger = AccessLogger
35 |
36 | def add_arguments(self, parser):
37 | parser.add_argument("path", default=".", nargs="?")
38 | parser.add_argument("--port", default=18000, help="Insert the port.")
39 | parser.add_argument("--domain", default="localhost", help="Insert the domain.")
40 | parser.add_argument("--kernel", default=None, help="Insert the kernel path.")
41 | parser.add_argument("--initfile", default=None, help="Insert the initfile path.")
42 | parser.add_argument(
43 | "--poolsize", default=1, help="Insert the kernel pool size.", type=int
44 | )
45 | parser.add_argument(
46 | "--startuptimeout",
47 | default=20,
48 | help="Startup timeout (in seconds) for kernels in the pool.",
49 | type=int,
50 | metavar="SECONDS",
51 | )
52 | parser.add_argument(
53 | "--cached",
54 | default=False,
55 | help="The server will cache the WL input expression.",
56 | action="store_true",
57 | )
58 | parser.add_argument(
59 | "--lazy",
60 | default=False,
61 | help="The server will start the kernels on the first request.",
62 | action="store_true",
63 | )
64 | parser.add_argument(
65 | "--index", default="index.wl", help="The file name to search for folder index."
66 | )
67 |
68 | parser.add_argument(
69 | "--demo",
70 | nargs="?",
71 | default=False,
72 | help="Run a demo application",
73 | choices=tuple(self.demo_choices.keys()),
74 | )
75 |
76 | parser.add_argument(
77 | "--client_max_size",
78 | default=10,
79 | dest="client_max_size",
80 | help="Maximum size of client uploads, in Mb",
81 | type=float,
82 | )
83 |
84 | def print_line(self, f="", s=""):
85 | self.print(f.ljust(15), s)
86 |
87 | def print_separator(self):
88 | self.print("-" * 70)
89 |
90 | @cached_property
91 | @to_dict
92 | def demo_choices(self):
93 |
94 | yield None, "form.wl"
95 |
96 | for path in os.listdir(self.demo_path()):
97 |
98 | if os.path.isdir(path):
99 | yield path, path
100 |
101 | if is_wl_code(path):
102 | yield first(os.path.splitext(path)), path
103 |
104 | def demo_path(self, *args):
105 | return module_path("wolframwebengine", "examples", "demo", *args)
106 |
107 | def handle(
108 | self,
109 | domain,
110 | port,
111 | path,
112 | kernel,
113 | poolsize,
114 | lazy,
115 | index,
116 | demo,
117 | initfile,
118 | startuptimeout,
119 | client_max_size,
120 | **opts
121 | ):
122 |
123 | if demo is None or demo:
124 | path = self.demo_path(self.demo_choices[demo])
125 |
126 | path = os.path.abspath(os.path.expanduser(path))
127 |
128 | client_max_size = int(client_max_size * (1024 ** 2))
129 |
130 | try:
131 | session = create_session(
132 | kernel, poolsize=poolsize, initfile=initfile, STARTUP_TIMEOUT=startuptimeout
133 | )
134 |
135 | except WolframKernelException as e:
136 | self.print(e)
137 | self.print("Use --help to display all available options.")
138 | sys.exit(1)
139 |
140 | loop = asyncio.get_event_loop()
141 |
142 | async def main():
143 |
144 | view = create_view(session, path, index=index, **opts)
145 |
146 | def request_factory(*args, **opts):
147 | return web.BaseRequest(
148 | *args, **opts, client_max_size=client_max_size, loop=loop
149 | )
150 |
151 | runner = self.ServerRunner(
152 | self.Server(
153 | view, access_log_class=self.AccessLogger, request_factory=request_factory
154 | )
155 | )
156 | await runner.setup()
157 | await self.TCPSite(runner, domain, port).start()
158 |
159 | self.print_separator()
160 |
161 | isdir = os.path.isdir(path)
162 |
163 | for args in (
164 | ("Address", "http://%s:%s/" % (domain, port)),
165 | (isdir and "Folder" or "File", path),
166 | ):
167 | self.print_line(*args)
168 |
169 | if isdir:
170 |
171 | if index:
172 | self.print_line("Index", index)
173 |
174 | if not os.path.exists(os.path.join(path, index)):
175 | self.print_separator()
176 | self.print_line(
177 | "Warning", "The folder %s doesn't contain an %s file." % (path, index)
178 | )
179 | self.print_line("", "No content will be served for the homepage.")
180 |
181 | self.print_separator()
182 |
183 | if sys.platform == "win32":
184 | self.print_line("(Press CTRL+BREAK to quit)")
185 | else:
186 | self.print_line("(Press CTRL+C to quit)")
187 |
188 | self.print_line()
189 |
190 | logging.basicConfig(level=logging.INFO, format="%(message)s")
191 |
192 | if not lazy:
193 | await session.start()
194 |
195 | while True:
196 | await asyncio.sleep(3600)
197 |
198 | try:
199 | loop.run_until_complete(main())
200 | except KeyboardInterrupt:
201 | if session.started:
202 | self.print("Requested server shutdown, closing session...")
203 | loop.run_until_complete(session.stop())
204 |
205 | loop.close()
206 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/commands/test.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from wolframclient.cli.commands.test import Command as TestCommand
4 | from wolframclient.utils.functional import iterate
5 |
6 |
7 | class Command(TestCommand):
8 |
9 | modules = ["wolframwebengine.tests"]
10 |
11 | dependencies = tuple(iterate((("django", "2.2.1"),), TestCommand.dependencies))
12 |
--------------------------------------------------------------------------------
/wolframwebengine/cli/dispatch.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from wolframclient.cli.dispatch import DispatchCommand as _DispatchCommand
4 |
5 |
6 | class DispatchCommand(_DispatchCommand):
7 |
8 | modules = [] + _DispatchCommand.modules + ["wolframwebengine.cli.commands"]
9 |
10 |
11 | def execute_from_command_line(argv=None, **opts):
12 | return DispatchCommand(argv).main()
13 |
--------------------------------------------------------------------------------
/wolframwebengine/examples/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
--------------------------------------------------------------------------------
/wolframwebengine/examples/aiohttp_application.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from aiohttp import web
4 | from wolframclient.evaluation import WolframEvaluatorPool
5 | from wolframclient.language import wl
6 |
7 | from wolframwebengine.web import aiohttp_wl_view
8 |
9 | session = WolframEvaluatorPool(poolsize=4)
10 | routes = web.RouteTableDef()
11 |
12 |
13 | @routes.get("/")
14 | async def hello(request):
15 | return web.Response(text="Hello from aiohttp")
16 |
17 |
18 | @routes.get("/form")
19 | @aiohttp_wl_view(session)
20 | async def form_view(request):
21 | return wl.FormFunction(
22 | {"x": "String"}, wl.Identity, AppearanceRules={"Title": "Hello from WL!"}
23 | )
24 |
25 |
26 | @routes.get("/api")
27 | @aiohttp_wl_view(session)
28 | async def api_view(request):
29 | return wl.APIFunction({"x": "String"}, wl.Identity)
30 |
31 |
32 | @routes.get("/app")
33 | @aiohttp_wl_view(session)
34 | async def app_view(request):
35 | return wl.Once(wl.Get("path/to/my/complex/wl/app.wl"))
36 |
37 |
38 | app = web.Application()
39 | app.add_routes(routes)
40 |
41 | if __name__ == "__main__":
42 | web.run_app(app)
43 |
--------------------------------------------------------------------------------
/wolframwebengine/examples/demo/ask.wl:
--------------------------------------------------------------------------------
1 | (* https://www.wolfram.com/language/11/cloud-and-web-interfaces/create-a-web-form-to-compute-the-marginal-tax-rate.html?product=language *)
2 |
3 | AskFunction[Module[{bracket, tax, income},
4 | bracket =
5 | Ask[{"brackets",
6 | "What is your marital status?"} -> {"Married filing jointly" -> {18550, 75300, 151900, 231450, 413350, 466950},
7 | "Single" -> {9275, 37650, 91150, 190150, 413350, 415050},
8 | "Head of household" -> {13250, 50400, 130150, 210800, 413350,
9 | 441000},
10 | "Married filing separately" -> {9275, 37650, 75950, 115725,
11 | 206675, 233475}}];
12 | income =
13 | Ask[{"income", "What was your income in the last year?"} ->
14 | Restricted["Number", {0, Infinity}]];
15 | tax = Integrate[
16 | Piecewise[{{.10 , 0 <= x <= bracket[[1]]}, {.15,
17 | bracket[[1]] < x <= bracket[[2]]}, {.25,
18 | bracket[[2]] < x <= bracket[[3]]}, {.28,
19 | bracket[[3]] < x <= bracket[[4]]}, {.33,
20 | bracket[[4]] < x <= bracket[[5]]}, {.35,
21 | bracket[[5]] < x <= bracket[[6]]}, {.396, True}}], {x, 0,
22 | income -
23 | If[Ask[{"deps",
24 | "Do you have any dependents?"} -> {"Yes" -> True,
25 | "No" -> False}],
26 | Ask[{"nodeps", "How many?"} ->
27 | Restricted["Integer", {0, Infinity}]]* 4050, 0]}];
28 | AskTemplateDisplay[
29 | Column[{"You owe $" <> TextString[tax] <> " in taxes.",
30 | "Your marginal tax rate is " <>
31 | TextString[Round[100.*tax/#income, 0.1]] <> "%",
32 | PieChart[{tax, income}]}] &]
33 | ]]
--------------------------------------------------------------------------------
/wolframwebengine/examples/demo/ca.wl:
--------------------------------------------------------------------------------
1 | (* https://www.wolfram.com/language/11/cloud-and-web-interfaces/create-a-complex-website-about-elementary-cellular.html?product=language *)
2 |
3 | URLDispatcher @ {
4 | "/" ~~ EndOfString -> GalleryView[
5 | Table[
6 | Hyperlink[
7 | ArrayPlot[CellularAutomaton[i, {{1}, 0}, {50, All}]],
8 | "/ca/" <>
9 | ToString[i]
10 | ],
11 | {i, {18, 22, 26, 28, 30, 50, 54, 45, 57, 73, 77, 60, 62, 105, 102, 126}}
12 | ]
13 | ],
14 | "/ca/" ~~ n : DigitCharacter .. ~~ EndOfString :> ExportForm[Column[{
15 | "Rule " <> n,
16 | RulePlot[CellularAutomaton[FromDigits[n]]],
17 | ArrayPlot[CellularAutomaton[FromDigits[n], {{1}, 0}, {50, All}]]
18 | }], "HTML"]
19 | }
--------------------------------------------------------------------------------
/wolframwebengine/examples/demo/form.wl:
--------------------------------------------------------------------------------
1 | FormPage[
2 | {"From" -> "City", "To" -> "City"},
3 | GeoGraphics[
4 | Style[Line[TravelDirections[{#From, #To}]], Thick, Red]
5 | ] &,
6 | AppearanceRules -> <|
7 | "Title" -> "Get travel directions for your trip",
8 | "Description" -> TemplateApply["This is a sample application running on a `` Kernel.", $VersionNumber]
9 | |>
10 | ]
--------------------------------------------------------------------------------
/wolframwebengine/examples/demo/trip.wl:
--------------------------------------------------------------------------------
1 | (* https://www.wolfram.com/language/11/cloud-and-web-interfaces/add-an-arbitrary-number-of-fields-to-a-form.html?product=language *)
2 |
3 | FormFunction[
4 | "city" -> RepeatingElement["City", {2, 5}],
5 | GeoGraphics[
6 | Append[GeoMarker /@ #city,
7 | Style[Line[TravelDirections[#city]], Thick, Red]], ImageSize -> 850] &,
8 | AppearanceRules -> <|
9 | "Title" -> "Get travel directions for your trip"|>]
--------------------------------------------------------------------------------
/wolframwebengine/examples/djangoapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/d1bdd91d8b754d1996ed8ca1949a2fa889344931/wolframwebengine/examples/djangoapp/__init__.py
--------------------------------------------------------------------------------
/wolframwebengine/examples/djangoapp/manage.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import sys
4 |
5 |
6 | def main():
7 | try:
8 | from django.conf import settings
9 | from django.core.management import execute_from_command_line
10 | except ImportError as exc:
11 | raise ImportError(
12 | "Couldn't import Django. Are you sure it's installed and "
13 | "available on your PYTHONPATH environment variable? Did you "
14 | "forget to activate a virtual environment?"
15 | ) from exc
16 |
17 | settings.configure(
18 | ROOT_URLCONF="wolframwebengine.examples.djangoapp.urls", ALLOWED_HOSTS="*", DEBUG=True
19 | )
20 |
21 | execute_from_command_line(sys.argv)
22 |
23 |
24 | if __name__ == "__main__":
25 | main()
26 |
--------------------------------------------------------------------------------
/wolframwebengine/examples/djangoapp/urls.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from django.http import HttpResponse
4 | from django.urls import path
5 | from wolframclient.evaluation import WolframLanguageSession
6 | from wolframclient.language import wl
7 |
8 | from wolframwebengine.web import django_wl_view
9 |
10 | session = WolframLanguageSession()
11 |
12 |
13 | def django_view(request):
14 | return HttpResponse("hello from django")
15 |
16 |
17 | @django_wl_view(session)
18 | def form_view(request):
19 | return wl.FormFunction({"x": "String"}, wl.Identity, "JSON")
20 |
21 |
22 | @django_wl_view(session)
23 | def api_view(request):
24 | return wl.APIFunction({"x": "String"}, wl.Identity, "JSON")
25 |
26 |
27 | urlpatterns = [
28 | path("", django_view, name="home"),
29 | path("form", form_view, name="form"),
30 | path("api", api_view, name="api"),
31 | ]
32 |
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/api/index.wl:
--------------------------------------------------------------------------------
1 | APIFunction["x" -> "String", Identity, "JSON"]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/foo/bar/index.wl:
--------------------------------------------------------------------------------
1 | "Hello from foo/bar"
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/foo/bar/something.wl:
--------------------------------------------------------------------------------
1 | "Hello from foo/bar/something"
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/foo/index.wl:
--------------------------------------------------------------------------------
1 | "Hello from foo"
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/foo/something.wl:
--------------------------------------------------------------------------------
1 | "Hello from foo/something"
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/form/index.wl:
--------------------------------------------------------------------------------
1 | FormFunction["x" -> "String", Identity, "JSON"]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/index.wl:
--------------------------------------------------------------------------------
1 | "Hello from / in a folder!"
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/random.wl:
--------------------------------------------------------------------------------
1 | RandomReal[]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/some.json:
--------------------------------------------------------------------------------
1 | ["hello", "from", "JSON", 0]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/some.m:
--------------------------------------------------------------------------------
1 | Delayed[{"hello", "from", "M", UnixTime[]}, "JSON"]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/some.mx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/d1bdd91d8b754d1996ed8ca1949a2fa889344931/wolframwebengine/examples/sampleapp/some.mx
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/some.wl:
--------------------------------------------------------------------------------
1 | Delayed[{"hello", "from", "WL", UnixTime[]}, "JSON"]
--------------------------------------------------------------------------------
/wolframwebengine/examples/sampleapp/some.wxf:
--------------------------------------------------------------------------------
1 | 8:fsDelayedfsListShelloSfromSWXFf sUnixTimeSJSON
--------------------------------------------------------------------------------
/wolframwebengine/server/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/d1bdd91d8b754d1996ed8ca1949a2fa889344931/wolframwebengine/server/__init__.py
--------------------------------------------------------------------------------
/wolframwebengine/server/app.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import logging
4 | import os
5 |
6 | from aiohttp import web
7 | from wolframclient.evaluation import WolframEvaluatorPool, WolframLanguageAsyncSession
8 | from wolframclient.language import wl
9 | from wolframclient.utils.functional import last
10 |
11 | from wolframwebengine.server.explorer import get_wl_handler_path_from_folder
12 | from wolframwebengine.web import aiohttp_wl_view
13 |
14 | EXTENSIONS = {
15 | ".wxf": wl.Composition(wl.BinaryDeserialize, wl.ReadByteArray),
16 | ".mx": wl.Function(wl.Import(wl.Slot(), "MX")),
17 | ".m": wl.Get,
18 | ".wl": wl.Get,
19 | }
20 |
21 |
22 | def is_wl_code(path):
23 | try:
24 | return bool(get_wl_handler(path))
25 | except KeyError:
26 | return False
27 |
28 |
29 | def get_wl_handler(path):
30 | return EXTENSIONS[last(os.path.splitext(path)).lower()]
31 |
32 |
33 | def create_session(
34 | path=None,
35 | poolsize=1,
36 | inputform_string_evaluation=False,
37 | kernel_loglevel=logging.INFO,
38 | **opts
39 | ):
40 | if poolsize <= 1:
41 | return WolframLanguageAsyncSession(
42 | path,
43 | inputform_string_evaluation=inputform_string_evaluation,
44 | kernel_loglevel=kernel_loglevel,
45 | **opts
46 | )
47 | return WolframEvaluatorPool(
48 | path,
49 | poolsize=poolsize,
50 | inputform_string_evaluation=inputform_string_evaluation,
51 | kernel_loglevel=kernel_loglevel,
52 | **opts
53 | )
54 |
55 |
56 | def create_view(
57 | session, path, cached=False, index="index.wl", path_extractor=lambda request: request.path
58 | ):
59 |
60 | path = os.path.abspath(os.path.expanduser(path))
61 |
62 | @aiohttp_wl_view(session)
63 | async def get_code(request, location=path):
64 | expr = get_wl_handler(location)(location)
65 | if cached:
66 | return wl.Once(expr)
67 | return expr
68 |
69 | if os.path.isdir(path):
70 |
71 | async def view(request):
72 | loc = get_wl_handler_path_from_folder(path, path_extractor(request), index=index)
73 |
74 | if not loc:
75 | return web.Response(body="Page not found", status=404)
76 |
77 | if is_wl_code(loc):
78 | return await get_code(request, location=loc)
79 | return web.FileResponse(loc)
80 |
81 | return view
82 |
83 | elif os.path.exists(path):
84 | if not is_wl_code(path):
85 | raise ValueError(
86 | "%s must be one of the following formats: %s"
87 | % (path, ", ".join(EXTENSIONS.keys()))
88 | )
89 | return get_code
90 | else:
91 | raise ValueError("%s is not an existing path on disk." % path)
92 |
--------------------------------------------------------------------------------
/wolframwebengine/server/explorer.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import os
4 |
5 |
6 | def get_wl_handler_path_from_folder(folder, path, index="index.wl"):
7 |
8 | absolute = os.path.join(folder, *filter(None, path.split("/")))
9 |
10 | if os.path.isdir(absolute):
11 | if index:
12 | absolute = os.path.join(absolute, index)
13 | else:
14 | return
15 |
16 | if os.path.exists(absolute):
17 | return absolute
18 |
--------------------------------------------------------------------------------
/wolframwebengine/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
--------------------------------------------------------------------------------
/wolframwebengine/tests/application_aiohttp.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from urllib.parse import urlparse
4 |
5 | from aiohttp import web
6 | from aiohttp.formdata import FormData
7 | from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
8 | from wolframclient.language import wl
9 | from wolframclient.utils.functional import first
10 | from wolframclient.utils.importutils import module_path
11 |
12 | from wolframwebengine.server.app import create_session, create_view
13 | from wolframwebengine.web import aiohttp_wl_view
14 | from wolframwebengine.web.utils import auto_wait
15 |
16 |
17 | class WolframEngineTestCase(AioHTTPTestCase):
18 | async def get_application(self):
19 |
20 | self.session = create_session(poolsize=1)
21 | routes = web.RouteTableDef()
22 |
23 | @routes.get("/")
24 | async def hello(request):
25 | return web.Response(text="Hello from aiohttp")
26 |
27 | @routes.get("/form")
28 | @routes.post("/form")
29 | @aiohttp_wl_view(self.session)
30 | async def form_view(request):
31 | return wl.FormFunction({"x": "String"}, wl.Identity, "JSON")
32 |
33 | @routes.get("/api")
34 | @routes.post("/api")
35 | @aiohttp_wl_view(self.session)
36 | async def api_view(request):
37 | return wl.APIFunction({"x": "String"}, wl.Identity, "JSON")
38 |
39 | @routes.get("/request/{name:.*}")
40 | @routes.post("/request/{name:.*}")
41 | @aiohttp_wl_view(self.session)
42 | async def request_view(request):
43 | return wl.Delayed(
44 | wl.HTTPRequestData(
45 | ["Method", "Scheme", "Domain", "PathString", "QueryString", "FormRules"]
46 | ),
47 | "JSON",
48 | )
49 |
50 | path = module_path("wolframwebengine", "examples", "sampleapp")
51 |
52 | for cached in (True, False):
53 |
54 | root = cached and "/cached" or "/app"
55 | view = create_view(
56 | session=self.session,
57 | path=path,
58 | cached=cached,
59 | path_extractor=lambda request, l=len(root): request.path[l:],
60 | index="index.wl",
61 | )
62 |
63 | routes.get(root + "{name:.*}")(view)
64 | routes.post(root + "{name:.*}")(view)
65 |
66 | app = web.Application()
67 | app.add_routes(routes)
68 |
69 | return app
70 |
71 | @unittest_run_loop
72 | async def test_aiohttp(self):
73 |
74 | for method, path, data in (
75 | ("GET", "/request/", []),
76 | ("GET", "/request/bar/bar?a=2", []),
77 | ("POST", "/request/some/random/path", {"a": "2"}),
78 | ):
79 |
80 | parsed = urlparse(path)
81 |
82 | resp = await self.client.request(method, path, data=data or None)
83 |
84 | self.assertEqual(resp.status, 200)
85 | self.assertEqual(
86 | await resp.json(),
87 | [method, "http", "127.0.0.1", parsed.path, parsed.query, data],
88 | )
89 |
90 | for cached in (True, False):
91 |
92 | root = cached and "/cached" or "/app"
93 |
94 | resp1 = await self.client.request("GET", root + "/random.wl")
95 | resp2 = await self.client.request("GET", root + "/random.wl")
96 |
97 | self.assertIsInstance(float(await resp1.text()), float)
98 | (cached and self.assertEqual or self.assertNotEqual)(
99 | await resp1.text(), await resp2.text()
100 | )
101 |
102 | for loc, content in (
103 | ("", '"Hello from / in a folder!"'),
104 | ("/", '"Hello from / in a folder!"'),
105 | ("/index.wl", '"Hello from / in a folder!"'),
106 | ("/foo", '"Hello from foo"'),
107 | ("/foo/", '"Hello from foo"'),
108 | ("/foo/index.wl", '"Hello from foo"'),
109 | ("/foo/bar", '"Hello from foo/bar"'),
110 | ("/foo/bar", '"Hello from foo/bar"'),
111 | ("/foo/bar/index.wl", '"Hello from foo/bar"'),
112 | ("/foo/bar/something.wl", '"Hello from foo/bar/something"'),
113 | ):
114 | resp = await self.client.request("GET", root + loc)
115 | self.assertEqual(resp.status, 200)
116 | self.assertEqual(await resp.text(), content)
117 |
118 | for loc in ("/some-random-url", "/404", "/some-/nonsense"):
119 | resp = await self.client.request("GET", root + loc)
120 | self.assertEqual(resp.status, 404)
121 |
122 | for fmt in ("wxf", "mx", "m", "wl", "json"):
123 |
124 | resp = await self.client.request("GET", root + "some." + fmt)
125 |
126 | self.assertEqual(resp.status, 200)
127 | self.assertEqual(len(await resp.json()), 4)
128 | self.assertEqual((await resp.json())[0:3], ["hello", "from", fmt.upper()])
129 | self.assertIsInstance((await resp.json())[-1], int)
130 |
131 | self.assertEqual(resp.headers["Content-Type"], "application/json")
132 |
133 | resp = await self.client.request("GET", "/")
134 |
135 | self.assertEqual(resp.status, 200)
136 | self.assertEqual(await resp.text(), "Hello from aiohttp")
137 |
138 | for root in ("", "/app"):
139 |
140 | resp = await self.client.request("GET", root + "/api")
141 |
142 | self.assertEqual(resp.status, 400)
143 | self.assertEqual((await resp.json())["Success"], False)
144 | self.assertEqual(resp.headers["Content-Type"], "application/json")
145 |
146 | resp = await self.client.request("GET", root + "/api?x=a")
147 |
148 | self.assertEqual(resp.status, 200)
149 | self.assertEqual((await resp.json())["x"], "a")
150 | self.assertEqual(resp.headers["Content-Type"], "application/json")
151 |
152 | resp = await self.client.request("GET", root + "/form")
153 |
154 | self.assertEqual(resp.status, 200)
155 | self.assertEqual(first(resp.headers["Content-Type"].split(";")), "text/html")
156 |
157 | resp = await self.client.request("POST", root + "/form", data={"x": "foobar"})
158 |
159 | self.assertEqual(resp.status, 200)
160 | self.assertEqual((await resp.json())["x"], "foobar")
161 | self.assertEqual(resp.headers["Content-Type"], "application/json")
162 |
163 | data = FormData()
164 | data.add_field("x", b"foobar", filename="somefile.txt")
165 |
166 | resp = await self.client.request("POST", root + "form", data=data)
167 |
168 | self.assertEqual(resp.status, 200)
169 | self.assertEqual((await resp.json())["x"], "foobar")
170 | self.assertEqual(resp.headers["Content-Type"], "application/json")
171 |
172 | def tearDown(self):
173 | if self.session.started:
174 | auto_wait(self.session.stop(), loop=self.loop)
175 | super().tearDown()
176 |
--------------------------------------------------------------------------------
/wolframwebengine/tests/application_django.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from django.core.files.uploadedfile import SimpleUploadedFile
4 | from django.test import Client
5 | from wolframclient.utils.api import json
6 | from wolframclient.utils.functional import first
7 | from wolframclient.utils.tests import TestCase as BaseTestCase
8 |
9 | from wolframwebengine.web.utils import auto_wait
10 |
11 |
12 | class DjangoTestCase(BaseTestCase):
13 | def setUp(self):
14 |
15 | from django.conf import settings
16 |
17 | settings.configure(
18 | ROOT_URLCONF="wolframwebengine.examples.djangoapp.urls",
19 | ALLOWED_HOSTS="*",
20 | DEBUG=True,
21 | )
22 |
23 | import django
24 |
25 | django.setup()
26 |
27 | from wolframwebengine.examples.djangoapp.urls import session
28 |
29 | self.session = session
30 | self.client = Client()
31 |
32 | def test_django_app(self):
33 |
34 | resp = self.client.get("/")
35 |
36 | self.assertEqual(resp.status_code, 200)
37 | self.assertEqual(resp.content, b"hello from django")
38 |
39 | resp = self.client.get("/form")
40 |
41 | self.assertEqual(resp.status_code, 200)
42 | self.assertEqual(first(resp["content-type"].split(";")), "text/html")
43 |
44 | resp = self.client.get("/api")
45 |
46 | self.assertEqual(resp.status_code, 400)
47 | self.assertEqual(resp["content-type"], "application/json")
48 |
49 | resp = self.client.get("/api?x=2")
50 |
51 | self.assertEqual(resp.status_code, 200)
52 | self.assertEqual(resp["content-type"], "application/json")
53 | self.assertEqual(json.loads(resp.content), {"x": "2"})
54 |
55 | resp = self.client.post("/api", {"x": SimpleUploadedFile("foo.txt", b"foobar")})
56 |
57 | self.assertEqual(resp.status_code, 200)
58 | self.assertEqual(resp["content-type"], "application/json")
59 | self.assertEqual(json.loads(resp.content), {"x": "foobar"})
60 |
61 | def tearDown(self):
62 | if self.session.started:
63 | auto_wait(self.session.stop())
64 | super().tearDown()
65 |
--------------------------------------------------------------------------------
/wolframwebengine/tests/folder_explorer.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import os
4 |
5 | from wolframclient.utils.importutils import module_path
6 | from wolframclient.utils.tests import TestCase as BaseTestCase
7 |
8 | from wolframwebengine.server.explorer import get_wl_handler_path_from_folder
9 |
10 |
11 | class TestCase(BaseTestCase):
12 | def test_sample_explorer(self):
13 |
14 | folder = module_path("wolframwebengine", "examples", "sampleapp")
15 |
16 | for path, resolved in (
17 | ("/", "index.wl"),
18 | ("/random.wl", "random.wl"),
19 | ("/foo/bar/", "foo/bar/index.wl"),
20 | ("/foo/", "foo/index.wl"),
21 | ("/foo/bar/index.wl", "foo/bar/index.wl"),
22 | ("/foo/bar/something.wl", "foo/bar/something.wl"),
23 | ):
24 |
25 | self.assertEqual(
26 | get_wl_handler_path_from_folder(folder, path), os.path.join(folder, resolved)
27 | )
28 |
--------------------------------------------------------------------------------
/wolframwebengine/web/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from functools import partial
4 |
5 | from wolframclient.utils.importutils import API
6 |
7 | from wolframwebengine.web.utils import is_coroutine_function
8 |
9 | available_backends = API(
10 | aiohttp="wolframwebengine.web.aiohttp.generate_http_response",
11 | django="wolframwebengine.web.django.generate_http_response",
12 | )
13 |
14 |
15 | def get_backend(backend):
16 | if not backend in available_backends:
17 | raise ValueError(
18 | "Invalid backend %s. Choices are: %s"
19 | % (backend, ", ".join(available_backends.keys()))
20 | )
21 | return available_backends[backend]
22 |
23 |
24 | def generate_http_response(session, backend):
25 | generator = get_backend(backend)
26 |
27 | def outer(func):
28 |
29 | if is_coroutine_function(func):
30 |
31 | async def inner(request, *args, **opts):
32 | return await generator(session, request, await func(request, *args, **opts))
33 |
34 | else:
35 |
36 | def inner(request, *args, **opts):
37 | return generator(session, request, func(request, *args, **opts))
38 |
39 | return inner
40 |
41 | return outer
42 |
43 |
44 | aiohttp_wl_view = partial(generate_http_response, backend="aiohttp")
45 | django_wl_view = partial(generate_http_response, backend="django")
46 |
--------------------------------------------------------------------------------
/wolframwebengine/web/aiohttp.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from functools import partial
4 |
5 | from wolframclient.language import wl
6 | from wolframclient.utils.api import aiohttp
7 | from wolframclient.utils.decorators import to_dict
8 | from wolframclient.utils.encoding import force_text
9 |
10 | from wolframwebengine.web.utils import (
11 | make_generate_httpresponse_expression,
12 | process_generate_httpresponse_expression,
13 | )
14 | from wolframwebengine.web.utils import to_multipart as _to_multipart
15 |
16 | to_multipart = partial(
17 | _to_multipart, namegetter=lambda f: f.filename, filegetter=lambda f: f.file
18 | )
19 |
20 |
21 | @to_dict
22 | def aiohttp_request_meta(request, post):
23 | yield "Method", request.method
24 | yield "Scheme", request.url.scheme
25 | yield "Domain", request.url.host
26 | yield "Port", force_text(request.url.port)
27 | yield "PathString", request.url.path
28 | yield "QueryString", request.url.query_string
29 | yield "Headers", tuple(wl.Rule(k, v) for k, v in request.headers.items())
30 | yield "MultipartElements", tuple(wl.Rule(k, to_multipart(v)) for k, v in post.items())
31 |
32 |
33 | async def generate_http_response(session, request, expression):
34 | wl_req = aiohttp_request_meta(request, await request.post())
35 |
36 | response = process_generate_httpresponse_expression(
37 | await session.evaluate(make_generate_httpresponse_expression(wl_req, expression))
38 | )
39 |
40 | return aiohttp.Response(
41 | body=response.get("BodyByteArray", b""),
42 | status=response.get("StatusCode", 200),
43 | headers=aiohttp.CIMultiDict(rule.args for rule in response.get("Headers", ())),
44 | )
45 |
--------------------------------------------------------------------------------
/wolframwebengine/web/django.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | from functools import partial
4 | from operator import attrgetter
5 |
6 | from django.http import HttpResponse
7 | from wolframclient.language import wl
8 | from wolframclient.utils.decorators import to_dict
9 | from wolframclient.utils.functional import first, iterate, last
10 |
11 | from wolframwebengine.web.utils import (
12 | auto_wait,
13 | make_generate_httpresponse_expression,
14 | process_generate_httpresponse_expression,
15 | )
16 | from wolframwebengine.web.utils import to_multipart as _to_multipart
17 |
18 | to_multipart = partial(_to_multipart, namegetter=attrgetter("name"))
19 |
20 |
21 | @to_dict
22 | def django_request_meta(request):
23 | yield "Method", request.method
24 | yield "Scheme", request.is_secure() and "https" or "http"
25 | yield "Domain", request.get_host()
26 | yield "Port", request.get_port()
27 | yield "PathString", request.path
28 | yield "QueryString", request.META["QUERY_STRING"]
29 | yield "Headers", tuple(wl.Rule(k, v) for k, v in request.headers.items())
30 | yield "MultipartElements", tuple(
31 | iterate(
32 | (
33 | wl.Rule(k, to_multipart(v))
34 | for k in request.POST.keys()
35 | for v in request.POST.getlist(k)
36 | ),
37 | (
38 | wl.Rule(k, to_multipart(v))
39 | for k in request.FILES.keys()
40 | for v in request.FILES.getlist(k)
41 | ),
42 | )
43 | )
44 |
45 |
46 | def generate_http_response(session, request, expression):
47 | wl_req = django_request_meta(request)
48 |
49 | response = process_generate_httpresponse_expression(
50 | auto_wait(session.evaluate(make_generate_httpresponse_expression(wl_req, expression)))
51 | )
52 |
53 | http = HttpResponse(
54 | content=response.get("BodyByteArray", b""), status=response.get("StatusCode", 200)
55 | )
56 |
57 | for rule in response.get("Headers", ()):
58 | http[first(rule.args)] = last(rule.args)
59 |
60 | return http
61 |
--------------------------------------------------------------------------------
/wolframwebengine/web/utils.py:
--------------------------------------------------------------------------------
1 | from __future__ import absolute_import, print_function, unicode_literals
2 |
3 | import inspect
4 | import os
5 | import shutil
6 | import tempfile
7 | import uuid
8 |
9 | from wolframclient.language import wl
10 | from wolframclient.serializers import export
11 | from wolframclient.utils import six
12 | from wolframclient.utils.asyncio import get_event_loop
13 | from wolframclient.utils.encoding import force_text
14 | from wolframclient.utils.functional import identity
15 |
16 | is_coroutine_function = getattr(inspect, "iscoroutinefunction", lambda: False)
17 | is_coroutine = getattr(inspect, "iscoroutine", lambda: False)
18 |
19 |
20 | def auto_wait(obj, loop=None):
21 | if is_coroutine(obj):
22 | return get_event_loop(loop).run_until_complete(obj)
23 | return obj
24 |
25 |
26 | def make_generate_httpresponse_expression(request, expression):
27 | return wl.GenerateHTTPResponse(wl.Unevaluated(expression), request)(
28 | ("BodyByteArray", "Headers", "StatusCode")
29 | )
30 |
31 |
32 | def process_generate_httpresponse_expression(response):
33 | if isinstance(response, dict):
34 | if not response.get("BodyByteArray", None):
35 | # empty byte array is returning a an empty list, we need an empty byte array
36 | response["BodyByteArray"] = b""
37 | return response
38 | return {
39 | "BodyByteArray": export(response, target_format="wl"),
40 | "Headers": (wl.Rule("content-type", "text/plain;charset=utf-8"),),
41 | "StatusCode": 500,
42 | }
43 |
44 |
45 | def to_multipart(v, namegetter=identity, filegetter=identity):
46 | if isinstance(v, six.string_types):
47 | return {"ContentString": v, "InMemory": True}
48 |
49 | destdir = os.path.join(tempfile.gettempdir(), force_text(uuid.uuid4()))
50 | os.mkdir(destdir)
51 |
52 | with open(os.path.join(destdir, namegetter(v)), "wb") as dest:
53 | shutil.copyfileobj(filegetter(v), dest)
54 | return {"FileName": dest.name, "InMemory": False, "OriginalFileName": namegetter(v)}
55 |
--------------------------------------------------------------------------------