├── .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 | ![image](https://raw.githubusercontent.com/WolframResearch/WolframWebEngineForPython/master/docs/assets/image1.png) 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:fsDelayedfsListShelloSfromSWXFfsUnixTimeSJSON -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------