├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── publish.yml │ └── tests.yml ├── .gitignore ├── BACKERS.md ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── artwork ├── LICENSE ├── logo-bwb-wb-sc.png ├── logo-bwb-xb-sc.png └── logo-bwb-xb-xl.png ├── docs ├── app_and_modules.md ├── auth.md ├── caching.md ├── cli.md ├── debug_and_logging.md ├── deployment.md ├── extensions.md ├── foreword.md ├── forms.md ├── html.md ├── installation.md ├── languages.md ├── mailer.md ├── orm.md ├── orm │ ├── advanced.md │ ├── callbacks.md │ ├── connecting.md │ ├── migrations.md │ ├── models.md │ ├── operations.md │ ├── relations.md │ ├── scopes.md │ └── virtuals.md ├── patterns.md ├── pipeline.md ├── quickstart.md ├── request.md ├── response.md ├── routing.md ├── services.md ├── sessions.md ├── templates.md ├── testing.md ├── tree.yml ├── tutorial.md ├── upgrading.md ├── validations.md └── websocket.md ├── emmett ├── __init__.py ├── __main__.py ├── __version__.py ├── _internal.py ├── _reloader.py ├── _shortcuts.py ├── app.py ├── asgi │ ├── __init__.py │ ├── handlers.py │ └── wrappers.py ├── assets │ ├── __init__.py │ ├── debug │ │ ├── __init__.py │ │ ├── shBrushPython.js │ │ ├── shCore.css │ │ ├── shCore.js │ │ ├── shTheme.css │ │ ├── view.css │ │ └── view.html │ ├── helpers.js │ ├── jquery.min.js │ └── jquery.min.map ├── cache.py ├── cli.py ├── ctx.py ├── datastructures.py ├── debug.py ├── extensions.py ├── forms.py ├── helpers.py ├── html.py ├── http.py ├── language │ ├── __init__.py │ ├── helpers.py │ └── translator.py ├── libs │ ├── __init__.py │ ├── contenttype.py │ └── portalocker.py ├── locals.py ├── orm │ ├── __init__.py │ ├── _patches.py │ ├── adapters.py │ ├── apis.py │ ├── base.py │ ├── connection.py │ ├── engines │ │ ├── __init__.py │ │ ├── postgres.py │ │ └── sqlite.py │ ├── errors.py │ ├── geo.py │ ├── helpers.py │ ├── migrations │ │ ├── __init__.py │ │ ├── base.py │ │ ├── commands.py │ │ ├── engine.py │ │ ├── exceptions.py │ │ ├── generation.py │ │ ├── helpers.py │ │ ├── migration.tmpl │ │ ├── operations.py │ │ ├── revisions.py │ │ ├── scripts.py │ │ └── utils.py │ ├── models.py │ ├── objects.py │ ├── transactions.py │ └── wrappers.py ├── parsers.py ├── pipeline.py ├── routing │ ├── __init__.py │ ├── response.py │ ├── router.py │ ├── routes.py │ ├── rules.py │ └── urls.py ├── rsgi │ ├── __init__.py │ ├── handlers.py │ └── wrappers.py ├── security.py ├── serializers.py ├── sessions.py ├── templating │ ├── __init__.py │ ├── lexers.py │ └── templater.py ├── testing.py ├── tools │ ├── __init__.py │ ├── auth │ │ ├── __init__.py │ │ ├── apis.py │ │ ├── exposer.py │ │ ├── ext.py │ │ ├── forms.py │ │ └── models.py │ ├── decorators.py │ ├── mailer.py │ ├── service.py │ └── stream.py ├── utils.py ├── validators │ ├── __init__.py │ ├── basic.py │ ├── consist.py │ ├── helpers.py │ ├── inside.py │ └── process.py └── wrappers │ ├── __init__.py │ ├── request.py │ ├── response.py │ └── websocket.py ├── examples └── bloggy │ ├── app.py │ ├── migrations │ └── 9d6518b3cdc2_first_migration.py │ ├── static │ └── style.css │ ├── templates │ ├── auth │ │ └── auth.html │ ├── index.html │ ├── layout.html │ ├── new_post.html │ └── one.html │ └── tests.py ├── pyproject.toml └── tests ├── helpers.py ├── languages ├── de.json ├── it.json └── ru.json ├── templates ├── auth │ └── auth.html ├── layout.html └── test.html ├── test_auth.py ├── test_cache.py ├── test_logger.py ├── test_mailer.py ├── test_migrations.py ├── test_orm.py ├── test_orm_connections.py ├── test_orm_gis.py ├── test_orm_pks.py ├── test_orm_row.py ├── test_orm_transactions.py ├── test_pipeline.py ├── test_routing.py ├── test_templates.py ├── test_translator.py ├── test_utils.py ├── test_validators.py └── test_wrappers.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [gi0baro] 2 | polar: emmett-framework 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | target-branch: "master" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | branches: 7 | - master 8 | 9 | env: 10 | PYTHON_VERSION: 3.12 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ env.PYTHON_VERSION }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ env.PYTHON_VERSION }} 22 | - name: Install uv 23 | uses: astral-sh/setup-uv@v5 24 | - name: Install dependencies 25 | run: | 26 | uv sync --dev 27 | - name: Lint 28 | run: | 29 | source .venv/bin/activate 30 | make lint 31 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: pypi 12 | url: https://pypi.org/p/emmett 13 | permissions: 14 | id-token: write 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.12 22 | - name: Install uv 23 | uses: astral-sh/setup-uv@v5 24 | - name: Build distributions 25 | run: | 26 | uv build 27 | - name: Publish package to pypi 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | with: 30 | skip-existing: true 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release 8 | pull_request: 9 | types: [opened, synchronize] 10 | branches: 11 | - master 12 | 13 | jobs: 14 | Linux: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] 20 | 21 | services: 22 | postgres: 23 | image: postgis/postgis:12-3.2 24 | env: 25 | POSTGRES_PASSWORD: postgres 26 | POSTGRES_DB: test 27 | ports: 28 | - 5432:5432 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Python ${{ matrix.python-version }} 33 | uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python-version }} 36 | - name: Install uv 37 | uses: astral-sh/setup-uv@v5 38 | - name: Install dependencies 39 | run: | 40 | uv sync --dev 41 | - name: Test 42 | env: 43 | POSTGRES_URI: postgres:postgres@localhost:5432/test 44 | run: | 45 | uv run pytest -v tests 46 | 47 | MacOS: 48 | runs-on: macos-latest 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | # FIXME: skipping 3.9 due to issues with `psycopg2-binary` 53 | # python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] 54 | python-version: ['3.10', '3.11', '3.12', '3.13'] 55 | 56 | steps: 57 | - uses: actions/checkout@v4 58 | - name: Set up Python ${{ matrix.python-version }} 59 | uses: actions/setup-python@v5 60 | with: 61 | python-version: ${{ matrix.python-version }} 62 | - name: Install uv 63 | uses: astral-sh/setup-uv@v5 64 | - name: Install dependencies 65 | run: | 66 | uv sync --dev 67 | - name: Test 68 | run: | 69 | uv run pytest -v tests 70 | 71 | Windows: 72 | runs-on: windows-latest 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] 77 | 78 | steps: 79 | - uses: actions/checkout@v4 80 | - name: Set up Python ${{ matrix.python-version }} 81 | uses: actions/setup-python@v5 82 | with: 83 | python-version: ${{ matrix.python-version }} 84 | - name: Install uv 85 | uses: astral-sh/setup-uv@v5 86 | - name: Install dependencies 87 | run: | 88 | uv sync --dev 89 | - name: Test 90 | shell: bash 91 | run: | 92 | uv run pytest -v tests 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.pyc 3 | __pycache__ 4 | 5 | *.sublime-* 6 | .venv 7 | .vscode 8 | 9 | .mypy_cache 10 | .pytest_cache 11 | 12 | build/* 13 | dist/* 14 | Emmett.egg-info/* 15 | poetry.lock 16 | uv.lock 17 | 18 | examples/*/databases 19 | examples/*/logs 20 | examples/*/private 21 | tests/databases/* 22 | tests/logs/* 23 | tests/private/* 24 | -------------------------------------------------------------------------------- /BACKERS.md: -------------------------------------------------------------------------------- 1 | # Sponsors & Backers 2 | 3 | Emmett is a BSD-licensed open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider: 4 | 5 | - [Become a backer or sponsor on Github](https://github.com/sponsors/gi0baro). 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Giovanni Barillari 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | pysources = emmett tests 3 | 4 | .PHONY: format 5 | format: 6 | ruff check --fix $(pysources) 7 | ruff format $(pysources) 8 | 9 | .PHONY: lint 10 | lint: 11 | ruff check $(pysources) 12 | ruff format --check $(pysources) 13 | 14 | .PHONY: test 15 | test: 16 | pytest -v tests 17 | 18 | .PHONY: all 19 | all: format lint test 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Emmett](https://emmett.sh/static/img/logo-bwb-xb-xl.png) 2 | 3 | Emmett is a full-stack Python web framework designed with simplicity in mind. 4 | 5 | The aim of Emmett is to be clearly understandable, easy to be learned and to be 6 | used, so you can focus completely on your product's features: 7 | 8 | ```python 9 | from emmett import App, request, response 10 | from emmett.orm import Database, Model, Field 11 | from emmett.tools import service, requires 12 | 13 | class Task(Model): 14 | name = Field.string() 15 | is_completed = Field.bool(default=False) 16 | 17 | app = App(__name__) 18 | app.config.db.uri = "postgres://user:password@localhost/foo" 19 | db = Database(app) 20 | db.define_models(Task) 21 | app.pipeline = [db.pipe] 22 | 23 | def is_authenticated(): 24 | return request.headers.get("api-key") == "foobar" 25 | 26 | def not_authorized(): 27 | response.status = 401 28 | return {'error': 'not authorized'} 29 | 30 | @app.route(methods='get') 31 | @requires(is_authenticated, otherwise=not_authorized) 32 | @service.json 33 | async def todo(): 34 | page = request.query_params.page or 1 35 | tasks = Task.where( 36 | lambda t: t.is_completed == False 37 | ).select(paginate=(page, 20)) 38 | return {'tasks': tasks} 39 | ``` 40 | 41 | ## Documentation 42 | 43 | The documentation is available at [https://emmett.sh/docs](https://emmett.sh/docs). 44 | The sources are available under the [docs folder](https://github.com/emmett-framework/emmett/tree/master/docs). 45 | 46 | ## Examples 47 | 48 | The *bloggy* example described in the [Tutorial](https://emmett.sh/docs/latest/tutorial) is available under the [examples folder](https://github.com/emmett-framework/emmett/tree/master/examples). 49 | 50 | ## Status of the project 51 | 52 | Emmett is production ready and is compatible with Python 3.9 and above versions. 53 | 54 | Emmett follows a *semantic versioning* for its releases, with a `{major}.{minor}.{patch}` scheme for versions numbers, where: 55 | 56 | - *major* versions might introduce breaking changes 57 | - *minor* versions usually introduce new features and might introduce deprecations 58 | - *patch* versions only introduce bug fixes 59 | 60 | Deprecations are kept in place for at least 3 minor versions, and the drop is always communicated in the [upgrade guide](https://emmett.sh/docs/latest/upgrading). 61 | 62 | ## How can I help? 63 | 64 | We would be very glad if you contributed to the project in one or all of these ways: 65 | 66 | * Talking about Emmett with friends and on the web 67 | * Adding issues and features requests here on GitHub 68 | * Participating in discussions about new features and issues here on GitHub 69 | * Improving the documentation 70 | * Forking the project and writing beautiful code 71 | 72 | ## License 73 | 74 | Emmett is released under the BSD License. 75 | 76 | However, due to original license limitations, contents under [validators](https://github.com/emmett-framework/emmett/tree/master/emmett/validators) and [libs](https://github.com/emmett-framework/emmett/tree/master/emmett/libs) are included in Emmett under their original licenses. Please check the source code for more details. 77 | -------------------------------------------------------------------------------- /artwork/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Giovanni Barillari 2 | 3 | This work is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 4 | -------------------------------------------------------------------------------- /artwork/logo-bwb-wb-sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/artwork/logo-bwb-wb-sc.png -------------------------------------------------------------------------------- /artwork/logo-bwb-xb-sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/artwork/logo-bwb-xb-sc.png -------------------------------------------------------------------------------- /artwork/logo-bwb-xb-xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/artwork/logo-bwb-xb-xl.png -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | Command Line Interface 2 | ====================== 3 | 4 | Emmett provides a built-in integration of the [click](http://click.pocoo.org) command line interface, to implement and allow customization of command line scripts. 5 | 6 | Basic Usage 7 | ----------- 8 | 9 | Emmett automatically installs a command `emmett` inside your virtualenv. The way this helper works is by providing access to all the commands on your Emmett application's instance, as well as some built-in commands that are included out of the box. Emmett extensions can also register more commands there if they desire to do so. 10 | 11 | For the `emmett` command to work, an application needs to be discovered. Emmett tries to automatic discover your application in the current working directory. In case Emmett fails to automatically detect your application, you can tell Emmett which application it should inspect, use the `--app` / `-a` parameter. It should be the import path for your application or the path to a Python file. 12 | 13 | Given that, to run a development server for your application, you can just write in your command line: 14 | 15 | ```bash 16 | > emmett develop 17 | ``` 18 | 19 | or, in the case of a single-file app: 20 | 21 | ```bash 22 | > emmett -a myapp.py develop 23 | ``` 24 | 25 | Running a Shell 26 | --------------- 27 | 28 | To run an interactive Python shell, you can use the `shell` command: 29 | 30 | ```bash 31 | > emmett shell 32 | ``` 33 | 34 | This will start up an interactive Python shell, setup the correct application context and setup the local variables in the shell. By default, you have access to your `app` object, and all the variables you defined in your application module. 35 | 36 | Custom Commands 37 | --------------- 38 | 39 | If you want to add more commands to the shell script, you can do this easily. 40 | In fact, if you want a shell command to setup your application, you can write: 41 | 42 | ```python 43 | from emmett import App 44 | 45 | app = App(__name__) 46 | 47 | @app.command('setup') 48 | def setup(): 49 | # awesome code to initialize your app 50 | ``` 51 | 52 | The command will then be available on the command line: 53 | 54 | ```bash 55 | > emmett setup 56 | ``` 57 | 58 | ### Command groups 59 | 60 | *New in version 2.2* 61 | 62 | You might also want to define several commands within the same *logical group*. In this scenario, the `command_group` decorator is what you're looking for: 63 | 64 | ```python 65 | @app.command_group('tasks') 66 | def tasks_cmd(): 67 | pass 68 | 69 | 70 | @tasks_cmd.command('create') 71 | def tasks_create_cmd(): 72 | # some code here 73 | ``` 74 | 75 | As you can see we defined a `tasks` command group, and a nested `create` command. We can invoke the upper command using: 76 | 77 | > emmett tasks create 78 | 79 | In case you need more information, please check the [click documentation](https://click.palletsprojects.com/en/7.x/commands/) about commands and groups. 80 | -------------------------------------------------------------------------------- /docs/debug_and_logging.md: -------------------------------------------------------------------------------- 1 | Debug and logging 2 | ================= 3 | 4 | *Errare humanum est*, said Seneca, a long time ago. As humans, sometimes we fail, and, sooner or later, we will see an exception on our applications. Even if the code is 100% correct, we can still get exceptions from time to time. And why? Well, *shit happens*, not only as a consequence of the Finagle's Law – even if he was damn right, wasn't he? – but also because the process of deploying web applications forces us to deal with a long list of involved technologies, everyone of which could fail. Just think about it: the client may fail during the request, your database can be overloaded, a hard-drive on your machine can crash, a library you're using can contain errors, and this goes on and on. 5 | 6 | So, what can we do to face all this necessary complexity? 7 | 8 | Emmett provides two facilities to track and debug errors on your application: a *debugger* for your development process, and a simple logging configuration for your production environment. 9 | 10 | Debugger 11 | -------- 12 | 13 | When you run your application with the built-in development server or set your `App.debug` attribute to `True`, Emmett will use its internal debugger when an exception occurs to show you some useful information. What does that look like? 14 | 15 | ![debugger](https://emmett.sh/static/screens/debug.png) 16 | 17 | The debug page contains three sections: 18 | 19 | - the **application traceback** 20 | - the **full traceback** 21 | - the **frames** view 22 | 23 | The difference between the two tracebacks is straightforward: the first is filtered only on your application code, while the second contains the complete trace of what happened – including the framework components, libraries, and so on. 24 | 25 | The third section of the debugger page is called *frames* and inspecting it can tell you a lot about what happened during an exception. 26 | 27 | ![debugger](https://emmett.sh/static/screens/debug_frames.png) 28 | 29 | As you can see, for every step of the full traceback, Emmett collects – when is possible – all the variables' contents and reports them as shown in the above screen. 30 | 31 | > – OK, dude. What happens when I have an error in a template? 32 | > – *the debugger catches them too.* 33 | 34 | ![debugger](https://emmett.sh/static/screens/debug_template.png) 35 | 36 | The debugger will also try to display the line that generated the exception in templates, complete with the error type. Still, when you forget a `pass` in a template file, it can be impossible to show you the statement that was not *passed*. 37 | 38 | Logging application errors 39 | -------------------------- 40 | 41 | When your application runs on production, Emmett – obviously – won't display the debug page, but will collect the full traceback and store it in logs. In fact, with the default configuration, a file called *production.log* will be created in the *logs* folder inside your application folder. It will log every message labeled as *warning* level or more severe. 42 | 43 | But how does Emmett logging works? 44 | 45 | It uses the standard Python logging module, and provides a shortcut that you can use with the `log` attribute of your `App`. This becomes handy when you want to add some messages inside your code, because you can just call: 46 | 47 | ```python 48 | app.log.debug('This is a debug message') 49 | app.log.info('This is an info message') 50 | app.log.warning('This is a warning message') 51 | ``` 52 | 53 | Basically, the `log` attribute of your app is a Python `Logger` with some handlers configured. As we said above, Emmett automatically logs exceptions calling your `app.log.exception()`. 54 | 55 | ### Configuring application logs 56 | 57 | You probably want to configure logging for your application to fit your needs. To do that, just use your `app.config` object: 58 | 59 | ```python 60 | from emmett import App, sdict 61 | 62 | app = App(__name__) 63 | 64 | app.config.logging.myfile = sdict( 65 | level="info", 66 | max_size=100*1024*1024 67 | ) 68 | ``` 69 | 70 | With this example, you will generate a *myfile.log* which will grow to 100MB in size and log all messages with an *info* level or higher. This is the complete list of parameters you can set for a logging file: 71 | 72 | | name | description | 73 | | --- | --- | 74 | | max\_size | max size for the logging files (default `5*1024*1024`) | 75 | | file\_no | number of old files to keep with rotation (default `4`) | 76 | | level | logging level (default `'warning'`) | 77 | | format | format for messages (default `'[%(asctime)s] %(levelname)s in %(module)s: %(message)s'`) | 78 | | on\_app\_debug | tells Emmett to log the messages also when application is in debug mode (default `False`) | 79 | 80 | That's it. 81 | -------------------------------------------------------------------------------- /docs/deployment.md: -------------------------------------------------------------------------------- 1 | Deployment 2 | ========== 3 | 4 | Depending on your setup and preferences, there are multiple ways to run Emmett applications. In this chapter, we'll try to document the most common ones. 5 | 6 | Included server 7 | --------------- 8 | 9 | *Changed in version 2.7* 10 | 11 | Emmett comes with [Granian](https://github.com/emmett-framework/granian) as its HTTP server. In order to run your application in production you can just use the included `serve` command: 12 | 13 | emmett serve --host 0.0.0.0 --port 80 14 | 15 | You can inspect all the available options of the `serve` command using the `--help` option. Here is the full list: 16 | 17 | | option | default | description | 18 | | --- | --- | --- | 19 | | host | 0.0.0.0 | Bind address | 20 | | port | 8000 | Bind port | 21 | | workers | 1 | Number of worker processes | 22 | | threads | 1 | Number of threads | 23 | | blocking-trheads | 1 | Number of blocking threads | 24 | | runtime-mode | st | Runtime implementation (possible values: st,mt) | 25 | | interface | rsgi | Server interface (possible values: rsgi,asgi) | 26 | | http | auto | HTTP protocol version (possible values: auto,1,2) | 27 | | http-read-timeout | 10000 | HTTP read timeout (in milliseconds) | 28 | | ws/no-ws | ws | Enable/disable websockets support | 29 | | loop | auto | Loop implementation (possible values: auto,asyncio,rloop,uvloop) | 30 | | log-level | info | Logging level (possible values: debug,info,warning,error,critical) | 31 | | backlog | 2048 | Maximum connection queue | 32 | | backpressure | | Maximum number of requests to process concurrently | 33 | | ssl-certfile | | Path to SSL certificate file | 34 | | ssl-keyfile | | Path to SSL key file | 35 | 36 | Other ASGI servers 37 | ------------------ 38 | 39 | *Changed in version 2.7* 40 | 41 | Since an Emmett application object is also an [ASGI](https://asgi.readthedocs.io/en/latest/) application, you can serve your project with any [ASGI compliant server](https://asgi.readthedocs.io/en/latest/implementations.html#servers). 42 | 43 | To serve your project with such servers, just refer to the specific server documentation an point it to your application object. 44 | 45 | Docker 46 | ------ 47 | 48 | Even if Docker is not properly a deployment option, we think giving an example of a `Dockerfile` for an Emmett application is proficient for deployment solutions using container orchestrators, such as Kubernetes or Mesos. 49 | 50 | In order to keep the image lighter, we suggest to use the *slim* Python image as a source: 51 | 52 | ```Dockerfile 53 | FROM python:3.9-slim 54 | 55 | RUN mkdir -p /usr/src/deps 56 | COPY requirements.txt /usr/src/deps 57 | 58 | WORKDIR /usr/src/deps 59 | RUN pip install --no-cache-dir -r /usr/src/deps/requirements.txt 60 | 61 | COPY ./ /app 62 | WORKDIR /app 63 | 64 | EXPOSE 8000 65 | 66 | CMD [ "emmett", "serve" ] 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/foreword.md: -------------------------------------------------------------------------------- 1 | 2 | Foreword 3 | ======== 4 | 5 | When I started writing web applications, the first "big decision" I faced was the choice of the programming language. 6 | 7 | As a programmer, it's not a big deal to work with different languages, and switching from a language to another, shouldn't be too hard, apart from learning a bit of syntax; at the same time everybody needs pick a language to use as a *daily habit*. We are humans, after all. 8 | 9 | I wanted a dynamic language to write my applications, a language with an easier syntax than PHP, and more structured than JavaScript. I wanted something that allowed me to write that *little magic* every developer does behind the scenes in a handy way. And I finally chose Python. Ruby was on the list (it seemed quite popular for web development at the time), but I've found its syntax, in some situation, less immediate; moreover, Python is somewhat more *solid* than Ruby – maybe even too much solid. I think even Python would be advantaged by a more dynamic community: just think about Python 3's adoption status (Ed.). 10 | 11 | I really enjoyed writing code in Python, and after gaining some confidence, I faced the second "big decision": which framework to use to write my applications Looking at the Python scene, I (obviously) started looking at *django*, the most famous one, but after a while I found I didn't like it. It wasn't as user friendly as I had hoped. Then I found *web2py*, and I loved it from the first line of the documentation book: it was simple, full of features, and learning it was much quicker than *django*. 12 | 13 | Nevertheless, after some years of using *web2py*, inspecting deeply the code and logic, and contributing it, I started having a feeling. A need grew in my mind while writing applications, to write things differently. I found myself thinking "Why should I write this stuff in *this* way? It's not cool or handy at all," and I had to face the problem that doing what I wanted would involve completely re-designing the whole framework. 14 | 15 | With this nagging feeling in my mind, I started looking around and found that a lot of the syntax and logic in *Flask* were the answer to what I was looking for. 16 | Unfortunately, at the same time, *Flask* had a lacked many of the features I was used to having out of the box with *web2py*, and not even using extensions would have been enough to cover it all. 17 | 18 | I naturally came to the conclusion that I was at *that point* of my coding life where I needed a "custom-designed tool". 19 | 20 | Why not? 21 | -------- 22 | 23 | > – Hey dude, what are you doing? 24 | > – *writing a new python web framework..* 25 | > – Whoa! Why would you do that? 26 | > – *...why not?* 27 | 28 | That was my answer when a friend of mine asked me the reasons behind my intention of building a new framework. It was a legitimate question: there are many frameworks on the scene. Is it really a good move to build a new tool rather than picking one of the available ones? 29 | 30 | I'd like to reply to this doubt with a definition of *tool* I really love: 31 | 32 | > **tool:** *something* intended to make a task easier. 33 | 34 | So a framework, which is a tool, has to let you write your application **easier** than without it. Now, I've found many frameworks – and I'm sure you can easily find them, too – where you have to deal with learning *a lot* of "how to do that" with the framework itself instead of focusing on the application. 35 | 36 | This is the first principle I've based *Emmett* on: **it should be easy to use and learn, so that you can focus on *your* product.** 37 | 38 | Another key principle of *Emmett* is the *preservation of your control* over the flow. What do I mean? There are several frameworks that do too much *magic* behind the scenes. I know that may sound weird because I've just talked about simplicity, but, if you think about it, you will find that a framework that is simple to use is not necessarily one which hides a lot of his flow. 39 | 40 | As developers, we have to admit that when we use frameworks or libraries for our project, many times it is hard to do something out of the ready-made scheme. I can think about several frameworks – even the famous *Ruby on Rails* – that, from time to time, force you to use a lot of formalism even when it's not really necessary. You find yourself writing code while following useless rules you don't like. 41 | 42 | In other words: I like magic too, but **isn't cooler when you actually *control* the magic?** 43 | 44 | With these principles in mind, I've tried to build a complete tool, something intended to make your task easier, with a rich set of features in the box. 45 | The result of my recipe is a framework which has an easy syntax, similar to *Flask*, but which also includes some of the lovable features of *web2py*. 46 | 47 | I hope you like it. 48 | 49 | Acknowledgments 50 | --------------- 51 | 52 | I would like to thank: 53 | 54 | * All the **Emmett contributors** 55 | * **Guido Van Rossum**, for the Python language 56 | * **Massimo Di Pierro** and **web2py's developers**, for what I learned from them and for their framework on which I based Emmett 57 | * **Armin Ronacher**, who really inspired me with the Flask framework 58 | * **Marco Stagni**, **Michael Genesini** and **Filippo Zanella** for their advices and continuous support 59 | -------------------------------------------------------------------------------- /docs/html.md: -------------------------------------------------------------------------------- 1 | HTML without templates 2 | ====================== 3 | 4 | As we saw in the [templates chapter](./templates), Emmett comes with a template engine out of the box, which you can use to render HTML. 5 | 6 | Under specific circumstances though, it might be convenient generating HTML directly in your route code, using the Python language. To support these scenarios, Emmett provides few helpers under the `html` module. Let's see them in details. 7 | 8 | The `tag` helper 9 | ---------------- 10 | 11 | The `tag` object is the main interface provided by Emmett to produce HTML contents from Python code. It dinamically produces HTML elements based on its attributes, so you can produce even custom elements: 12 | 13 | ```python 14 | from emmett.html import tag 15 | 16 | # an empty

17 | p = tag.p() 18 | # a custom element 19 | card = tag.card() 20 | # a custom element 21 | list_item = tag["list-item"]() 22 | ``` 23 | 24 | Every element produced by the `tag` helper accepts both nested contents and attributes, with the caveat HTML attributes needs to start with `_`: 25 | 26 | ```python 27 | #

Hello world

28 | p = tag.p("Hello world") 29 | #

bar

30 | div = tag.div(tag.p("bar"), _class="foo") 31 | ``` 32 | 33 | > **Note:** the reasons behind the underscore notation for HTML attributes are mainly: 34 | > - to avoid issues with Python reserved words (eg: `class`) 35 | > - to keep the ability to set custom attributes on the HTML objects in Python code but prevent those attributes to be rendered 36 | 37 | Mind that the `tag` helper already takes care of *self-closing* elements and escaping contents, so you don't have to worry about those. 38 | 39 | > – That's cool dude, but what if I need to set several attributes with the same prefix? 40 | > – *Like with HTMX? Sure, just use a dictionary* 41 | 42 | ```python 43 | # 44 | btn = tag.button( 45 | "Click me", 46 | _hx={ 47 | "post": url("clicked"), 48 | "swap": "outerHTML" 49 | } 50 | ) 51 | ``` 52 | 53 | The `cat` helper 54 | ---------------- 55 | 56 | Sometimes you may need to stack together HTML elements without a parent. For such cases, the `cat` helper can be used: 57 | 58 | ```python 59 | from emmett.html import cat, tag 60 | 61 | #

hello

world

62 | multi_p = cat(tag.p("hello"), tag.p("world")) 63 | ``` 64 | 65 | Building deep stacks 66 | -------------------- 67 | 68 | All the elements produced with the `tag` helper supports `with` statements, so you can easily manage even complicated stacks. For instance the following code: 69 | 70 | ```python 71 | root = tag.div(_class="root") 72 | with root: 73 | with tag.div(_class="lv1"): 74 | with tag.div(_class="lvl2"): 75 | tag.p("foo") 76 | tag.p("bar") 77 | 78 | str(root) 79 | ``` 80 | 81 | will produce the following HTML: 82 | 83 | ```html 84 |
85 |
86 |
87 |

foo

88 |

bar

89 |
90 |
91 |
92 | ``` 93 | 94 | > **Note:** when compared to templates, HTML generation from Python will be noticeably slower. For cases in which you want to render long and almost static HTML contents, using templates is preferable. 95 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | 2 | Installation 3 | ============ 4 | 5 | So, how do you get Emmett on your computer quickly? There are many ways you could do that, but the most kick-ass method is virtualenv, so let’s have a look at that first. 6 | 7 | You will need Python version 3.9 or higher in order to get Emmett working. 8 | 9 | virtualenv 10 | ---------- 11 | 12 | Virtualenv is probably what you want to use during development, and if you have shell access to your production machines, you’ll probably want to use it there, too. 13 | 14 | What problem does virtualenv solve? If you use Python a bit, you'll probably want to use it for other projects besides Emmett-based web applications. However, the more projects you have, the more likely it is that you will be working with different versions of Python itself, or at least different versions of Python libraries. Let’s face it: quite often, libraries break backwards compatibility, and it’s unlikely that any serious application will have zero dependencies. So what do you do if two or more of your projects have conflicting dependencies? 15 | 16 | Virtualenv to the rescue! Virtualenv enables multiple side-by-side installations of Python, one for each project. It doesn’t actually install separate copies of Python, but it does provide a clever way to keep different project environments isolated. 17 | Let’s see how virtualenv works. 18 | 19 | #### virtualenv on Python 3 20 | 21 | You can just initialize your environment in the *.venv* folder using: 22 | 23 | ```bash 24 | $ mkdir -p myproject 25 | $ cd myproject 26 | $ python -m venv .venv 27 | ``` 28 | 29 | ### Installing Emmett on virtualenv 30 | 31 | Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, you can do the following: 32 | 33 | ```bash 34 | $ source .venv/bin/activate 35 | ``` 36 | 37 | You should now be using your virtualenv (notice how the prompt of your shell has changed to show the active environment). 38 | 39 | Now you can just enter the following command to get Emmett activated in your virtualenv: 40 | 41 | ```bash 42 | $ pip install emmett 43 | ``` 44 | 45 | And now you are good to go. 46 | 47 | You can read more about virtualenv on [its documentation website](https://docs.python.org/3/library/venv.html). 48 | -------------------------------------------------------------------------------- /docs/languages.md: -------------------------------------------------------------------------------- 1 | Languages and internationalization 2 | ================================== 3 | 4 | Emmett provides *Severus* as its integrated internationalization engine for managing multiple languages in your application. How does it work? 5 | 6 | ```python 7 | from emmett import App, T 8 | app = App(__name__) 9 | 10 | @app.route("/") 11 | async def index(): 12 | hello = T('Hello, my dear!') 13 | return dict(hello=hello) 14 | ``` 15 | 16 | As you can see, Emmett expose a language translator with the `T` object. 17 | It tells Emmett to translate the string depending on clients locale, and is also available in the templating environment. 18 | 19 | So what you should do with the other languages' text? You can just leave it in *json* or *yaml* files within a *languages* directory. Each file should be named after the international symbol for the language. For example, an Italian translation should be stored as *languages/it.json*: 20 | 21 | ```json 22 | { 23 | "Hello, my dear!": "Ciao, mio caro!" 24 | } 25 | ``` 26 | 27 | and the hello message will be translated when the user request the Italian language. 28 | 29 | > – awesome. Though, how does Emmett decides which language should be loaded for 30 | a specific client? 31 | > – *actually, you can choose that* 32 | 33 | Emmett has two different ways to handle languages in your application: 34 | 35 | * using clients' HTTP headers 36 | * using URLs 37 | 38 | Let's see the differences between the two systems. 39 | 40 | Translation using HTTP headers 41 | ------------------------------ 42 | 43 | Let's say your application has English for its default language and you want your application to also be available in Italian, as the above example. 44 | With default settings, the user's requested language is determined by the "Accept-Language" field in the HTTP header. This means that if *User 1* has their browser set to accept Italian, he will see the Italian version when visiting 'http://127.0.0.1:8000/'. Meanwhile, if *User 2* has their browser set for any other language, they will see the english one. 45 | 46 | Using this translation technique is quite easy. The available languages are 47 | defined automatically, based on the translation files you have inside *languages* 48 | folder of your application. 49 | 50 | ``` 51 | /myapp 52 | /languages 53 | it.json 54 | ``` 55 | 56 | will make your application available in Italian in addition to the English written in your templates and exposed functions. 57 | 58 | Simply add more translation programs to increase your multi-language support: 59 | 60 | ``` 61 | /myapp 62 | /languages 63 | it.json 64 | es.yaml 65 | ``` 66 | 67 | will make your application available in English, Italian and Spanish. 68 | 69 | You can change the default language of your application with the following line in the file where you wrote your application's exposed functions: 70 | 71 | ```python 72 | app.language_default = 'it' 73 | ``` 74 | 75 | Translation using routing 76 | ------------------------- 77 | 78 | There are many scenarios where you want your application to use different URLs to separate contents based on the language. 79 | 80 | Let's say again you have your application with English as the default language and you provide a supplementary Italian version; to achieve the routing translation behavior under Emmett, you should write: 81 | 82 | ```python 83 | app.languages = ['en', 'it'] 84 | app.language_default = 'en' 85 | app.language_force_on_url = True 86 | ``` 87 | and Emmett will automatically add the support for language on your routing rules to the follow: 88 | 89 | | requested URL | behaviour | 90 | | --- | --- | 91 | | /anexampleurl | shows up the contents with the default language | 92 | | /it/anexampleurl | shows up the contents with the italian language | 93 | 94 | As you can see, the *routing* way requires that you to explicitly tell to Emmett which languages should be available into your application, so it can build the routing tables. 95 | -------------------------------------------------------------------------------- /docs/mailer.md: -------------------------------------------------------------------------------- 1 | Sending mails 2 | ============= 3 | 4 | Sooner or later you will need to send mails to your users from your application. Emmett provides a simple interface to set up SMTP with your application and to send messages to your users. 5 | 6 | Let's start configuring a simple mailer within our application: 7 | 8 | ```python 9 | from emmett import App 10 | from emmett.tools import Mailer 11 | 12 | app = App(__name__) 13 | app.config.mailer.sender = "nina@massivedynamic.com" 14 | 15 | mailer = Mailer(app) 16 | ``` 17 | 18 | With just these lines the mailer is ready to send messages, using the local machine as the SMTP server and sending messages from the address *nina@massivedynamic.com*. 19 | 20 | The mailer also accepts additional configuration parameters, here is the complete list: 21 | 22 | | parameter | default value | description | 23 | | --- | --- | --- | 24 | | sender | `None` | the address to use for the *From* value | 25 | | server | 127.0.0.1 | the SMTP host to use | 26 | | port | 25 | the SMTP port to use | 27 | | username | `None` | the username to authenticate with (if needed) | 28 | | password | `None` | the password to authenticate with (if needed) | 29 | | use\_tls | `False` | decide if TLS should be used | 30 | | use\_ssl | `False` | decide if SSL should be used | 31 | 32 | Now, let's see how to send messages. 33 | 34 | Sending messages 35 | ---------------- 36 | 37 | We can create a simple message using the `mail` method of our mailer: 38 | 39 | ```python 40 | message = mailer.mail( 41 | subject="Hello", 42 | body="A very important message", 43 | recipients=["walter@massivedynamic.com"]) 44 | ``` 45 | 46 | and add another recipient later: 47 | 48 | ```python 49 | message.add_recipient("william@massivedynamics.com") 50 | ``` 51 | 52 | and when we are ready, we can just send it: 53 | 54 | ```python 55 | message.send() 56 | ``` 57 | 58 | We can also create a message and send it directly: 59 | 60 | ```python 61 | mailer.send_mail( 62 | subject="Hello", 63 | body="A very important message", 64 | recipients=["walter@massivedynamic.com"]) 65 | ``` 66 | 67 | or set an html content for the message: 68 | 69 | ```python 70 | message.html = "Testing" 71 | ``` 72 | 73 | Attachments 74 | ----------- 75 | 76 | Once you created a message, adding attachments is quite easy: 77 | 78 | ```python 79 | msg = mailer.mail(subject="See this") 80 | 81 | with open("image.png") as fp: 82 | msg.attach(filename="image.png", data=fp.read()) 83 | 84 | msg.recipients = ["walter@massivedynamic.com"] 85 | msg.send() 86 | ``` 87 | 88 | > **Note:** The default encoding used by the mailer is utf-8. 89 | 90 | Tests and suppression 91 | --------------------- 92 | 93 | When you are testing your application, or if you are in a development environment, it’s useful to be able to suppress email sending. Still, you may want to test some message was generated in your code, and you want to catch it. 94 | 95 | The mailer provide a `store_mails` method for this, so you can just write down: 96 | 97 | ```python 98 | with mailer.store_mails() as outbox: 99 | mailer.send_mail( 100 | subject='testing', body='test', recipients=["foo@bar.com"]) 101 | assert len(outbox) == 1 102 | assert outbox[0].subject == "testing" 103 | ``` 104 | 105 | and the mailer just avoid the *real sending* of the message. 106 | 107 | If you want to totally disable email sending, you can use the `suppress` parameter in the configuration: 108 | 109 | ```python 110 | app.config.mailer.suppress = True 111 | ``` 112 | 113 | This will completely disable message sending. 114 | -------------------------------------------------------------------------------- /docs/orm.md: -------------------------------------------------------------------------------- 1 | Using databases 2 | =============== 3 | 4 | > – OK, what if I need to use a database in my application? 5 | > – *you can use the included ORM* 6 | 7 | Emmett comes with an integrated ORM based on [pyDAL](https://github.com/web2py/pydal), which gives you the ability to use a database in your application writing simple Python code without worrying about queries and specific syntax of the database engine you want to use. 8 | 9 | Thanks to this database layer, you can write the same code and use the same syntax independently of which of the available adapters you want to use during development or when you're deploying your app to the world. 10 | 11 | This is the list of the supported database engines, where we included the name used by Emmett for the connection configuration and the appropriate driver you need to install, separately from Emmett (just use pip): 12 | 13 | | Supported DBMS | adapter name | python driver | 14 | | --- | --- | --- | 15 | | SQLite | sqlite | | 16 | | PostgreSQL | postgres | psycopg2, pyscopg 3 (experimental), pg8000, zxjdbc | 17 | | MySQL | mysql | pymysql, mysqldb | 18 | | MSSQL | mssql | pyodbc | 19 | | MongoDB | mongo | pymongo | 20 | 21 | The next database engines are included in Emmett but are targeted with *experimental* support, as they're not officially supported by the Emmett development: 22 | 23 | | Experimental support | adapter name | python driver(s) | 24 | | --- | --- | --- | 25 | | CouchDB | couchdb | couchdb | 26 | | Oracle | oracle | cxoracle | 27 | | FireBird | firebird | kinterbasdb, fdb, pyodbc | 28 | | DB2 | db2 | pyodbc | 29 | | Informix | informix | informixdb | 30 | | Ingres | ingres | ingresdbi | 31 | | Cubrid | cubrid | cubridb | 32 | | Sybase | sybase | Sybase | 33 | | Teradata | teradata | pyodbc | 34 | | SAPDB | sapdb | sapdb | 35 | 36 | > **Note:** 37 | > This list may change, and depends on the engine support of pyDAL. For any 38 | further information, please check out the [project page](https://github.com/web2py/pydal). 39 | 40 | So, how do you use Emmett's ORM? Let's see it with an example: 41 | 42 | ```python 43 | from emmett import App 44 | from emmett.orm import Database, Model, Field 45 | 46 | app = App(__name__) 47 | app.config.db.uri = "sqlite://storage.sqlite" 48 | 49 | class Post(Model): 50 | author = Field() 51 | title = Field() 52 | body = Field.text() 53 | 54 | db = Database(app) 55 | db.define_models(Post) 56 | 57 | app.pipeline = [db.pipe] 58 | 59 | @app.route('/posts/') 60 | def post_by(author): 61 | posts = db(Post.author == author).select() 62 | return dict(posts=posts) 63 | ``` 64 | 65 | The above code is quite simple: the `post_by()` function lists posts from a 66 | specific author. Let's retrace what we done in those simple lines: 67 | 68 | * we added an *sqlite* database to our application, stored on file *storage.sqlite* 69 | * we defined the *Post* model and its properties, which will create a *posts* table 70 | * we registered the database pipe to our application's pipeline so that it will be available during requests 71 | * we did a select on the *posts* table querying the *author* column 72 | 73 | As you noticed, the fields defined for the table are available for queries as 74 | attributes, and calling *db* with a query argument provides you a set on 75 | which you can do operations like the `select()`. 76 | 77 | In the next chapters, we will inspect how to define models, all the available options, 78 | and how to use Emmett's ORM to perform operations on the database. 79 | -------------------------------------------------------------------------------- /docs/patterns.md: -------------------------------------------------------------------------------- 1 | Patterns for Emmett 2 | =================== 3 | 4 | Emmett is crafted to fit the needs of many applications, from the smallest to the 5 | largest ones. Due to this, your application can be built up from a single Python 6 | file and scale to a better organized structure. 7 | 8 | In this section, we will cover some good *patterns* you may follow when your 9 | application starts becoming large, or when you just need to organize your code 10 | better. 11 | 12 | Package pattern 13 | --------------- 14 | 15 | The package pattern will make your application a Python package instead of a module. 16 | For instance, let's assume your original application is structured like that: 17 | 18 | ``` 19 | /myapp 20 | myapp.py 21 | /static 22 | style.css 23 | /templates 24 | layout.html 25 | index.html 26 | login.html 27 | ... 28 | ``` 29 | 30 | To convert it to a package application, you should create another folder inside 31 | your original *myapp* one, and rename *myapp.py* to *\__init__.py*, ending up 32 | with something like this: 33 | 34 | ``` 35 | /myapp 36 | /myapp 37 | __init__.py 38 | /static 39 | style.css 40 | /templates 41 | layout.html 42 | index.html 43 | login.html 44 | ... 45 | ``` 46 | 47 | > – OK, dude. But what did we gain by doing this? 48 | > – *well, now we can organize the code in multiple modules* 49 | 50 | With this new structure, we can create a new *views.py* file inside the package 51 | and we can move the routed functions to it. For example, your *\__init__.py* file 52 | can look like this: 53 | 54 | ```python 55 | from emmett import App 56 | 57 | app = App(__name__) 58 | 59 | from . import views 60 | ``` 61 | 62 | and your *views.py* could look like: 63 | 64 | ```python 65 | from . import app 66 | 67 | @app.route("/") 68 | async def index(): 69 | # some code 70 | ``` 71 | 72 | Your final structure would be like this: 73 | 74 | ``` 75 | /myapp 76 | /myapp 77 | __init__.py 78 | views.py 79 | /static 80 | style.css 81 | /templates 82 | layout.html 83 | index.html 84 | login.html 85 | ... 86 | ``` 87 | 88 | > – That's nice, but how can I run my application now? 89 | 90 | You can use the Emmett command inside the original directory of your application: 91 | 92 | ```bash 93 | $ emmett -a myapp develop 94 | ``` 95 | 96 | > **A note regarding circular imports:** 97 | > Every Python developer hates them, and yet we just added some of them: *views.py* depends on *\_\_init\_\_.py* while *\_\_init\_\_.py* imports *views.py*. In general, this is a bad idea, but it is actually fine here because we are not actually using the views in *\_\_init\_\_.py*. We are ensuring that the module is imported to expose the functions; also, we are doing that at the bottom of the file. 98 | 99 | MVC pattern 100 | ----------- 101 | The **MVC** (Model-View-Controller) pattern, used widely in web applications, is well structured and becomes handy when you have big applications. Emmett does not provide controllers, but you can implement an MVC pattern using application modules. An MVC structure for an Emmett application can look something like this: 102 | 103 | ``` 104 | /myapp 105 | __init__.py 106 | /controllers 107 | __init__.py 108 | main.py 109 | api.py 110 | /models 111 | __init__.py 112 | user.py 113 | article.py 114 | /templates 115 | layout.html 116 | index.html 117 | login.html 118 | ... 119 | ``` 120 | 121 | As you can see, it's an extension of the *package pattern*, where we added the 122 | two sub-packages *controllers* and *models*, each with an empty *\_\_init\_\_.py* file. 123 | 124 | With this structure, your application's *\_\_init\_\_.py* would look like this: 125 | 126 | ```python 127 | from emmett import App 128 | from emmett.orm import Database 129 | 130 | app = App(__name__) 131 | app.config.url_default_namespace = "main" 132 | 133 | db = Database() 134 | 135 | from .models.user import User 136 | from .models.article import Post 137 | db.define_models(User, Post) 138 | 139 | from .controllers import main, api 140 | ``` 141 | 142 | We told Emmett to use the *main.py* controller as default for urls, so we can just 143 | call `url('index')` instead of `url('main.function')` in our application. 144 | 145 | The main controller can look like this: 146 | 147 | ```python 148 | from .. import app 149 | 150 | @app.route("/") 151 | async def index(): 152 | # code 153 | ``` 154 | 155 | and the *api.py* controller can look like this: 156 | 157 | ```python 158 | from .. import app 159 | 160 | api = app.module(__name__, 'api', url_prefix='api') 161 | 162 | @api.route() 163 | async def a(): 164 | # code 165 | ``` 166 | -------------------------------------------------------------------------------- /docs/services.md: -------------------------------------------------------------------------------- 1 | Services 2 | ======== 3 | 4 | Quite often, you will need to render the output of your application using a 5 | protocol other than HTML; for example, JSON or XML. 6 | 7 | Emmett can help you expose those services with the `service` decorator: 8 | 9 | ```python 10 | from emmett import App 11 | from emmett.tools import service 12 | 13 | app = App(__name__) 14 | 15 | @app.route("/json") 16 | @service.json 17 | async def f(): 18 | # your code 19 | ``` 20 | The output will be automatically converted using the required service 21 | (JSON in this example). 22 | 23 | > – awesome. But, what if I need to expose several function with a service? 24 | Should I decorate every function? 25 | > – *you can use the provided pipe, dude* 26 | 27 | Emmett also provides a `ServicePipe` object so you can create an application module with all the functions you want to expose with a specific service and add the pipe to the module: 28 | 29 | ```python 30 | from emmett.tools import ServicePipe 31 | from myapp import app 32 | 33 | api = app.module(__name__, 'api') 34 | api.pipeline = [ServicePipe('json')] 35 | 36 | @api.route() 37 | async def a(): 38 | # code 39 | 40 | @api.route() 41 | async def b(): 42 | # code 43 | ``` 44 | 45 | So, which are the available services? Let's see them. 46 | 47 | JSON and XML 48 | ------------ 49 | 50 | Providing a JSON service with Emmett is quite easy: 51 | 52 | ```python 53 | @app.route("/json") 54 | @service.json 55 | async def f(): 56 | l = [1, 2, {'foo': 'bar'}] 57 | return dict(status="OK", data=l) 58 | ``` 59 | 60 | The output will be a JSON object with the converted content of your python 61 | dictionary: 62 | 63 | ```json 64 | { 65 | "status": "OK", 66 | "data": [ 67 | 1, 68 | 2, 69 | { 70 | "foo": "bar", 71 | } 72 | ] 73 | } 74 | ``` 75 | 76 | To provide an XML service, just decorate your function using the next line 77 | instead: 78 | 79 | ```python 80 | @service.xml 81 | ``` 82 | 83 | Obviously, the syntax for using `ServicePipe` is the same as in the 84 | first example: 85 | 86 | ```python 87 | # providing a JSON service pipe 88 | ServicePipe('json') 89 | 90 | # providing an XML service pipe 91 | ServicePipe('xml') 92 | ``` 93 | 94 | Multiple services 95 | ----------------- 96 | 97 | Sometimes you may want to expose several services for a single endpoint, for example a list of items both in JSON and XML format. 98 | 99 | You can easily achieve this decorating your route multiple times, using different pipelines: 100 | 101 | ```python 102 | from emmett.tools import ServicePipe 103 | 104 | @app.route('/elements.json', pipeline=[ServicePipe('json')]) 105 | @app.route('/elements.xml', pipeline=[ServicePipe('xml')]) 106 | async def elements(): 107 | return [{"foo": "bar"}, {"bar": "foo"}] 108 | ``` 109 | 110 | With this notation, you can serve different services using the same exposed method. 111 | -------------------------------------------------------------------------------- /docs/sessions.md: -------------------------------------------------------------------------------- 1 | Sessions 2 | ======== 3 | 4 | An essential feature for a web application is the ability to store specific informations about the client from a request to the next one. Accordingly to this need, Emmett provides another object beside the `request` and the `response` ones called `session`. 5 | 6 | ```python 7 | from emmett import session 8 | 9 | @app.route("/counter") 10 | async def count(): 11 | session.counter = (session.counter or 0) + 1 12 | return "This is your %d visit" % session.counter 13 | ``` 14 | 15 | The above code is quite simple: the app increments the counter every time the user visit the page and return this number to the user. 16 | Basically, you can use `session` object to store and retrieve data, but before you can do that, you should add a *SessionManager* to your application pipeline. These managers allows you to store sessions' data on different storage systems, depending on your needs. Let's see them in detail. 17 | 18 | Storing sessions in cookies 19 | --------------------------- 20 | 21 | *Changed in version 2.5* 22 | 23 | You can store session contents directly in the cookies of the client using the Emmett's `SessionManager.cookies` pipe: 24 | 25 | ```python 26 | from emmett import App, session 27 | from emmett.sessions import SessionManager 28 | 29 | app = App(__name__) 30 | app.pipeline = [SessionManager.cookies('myverysecretkey')] 31 | 32 | @app.route("/counter") 33 | # previous code 34 | ``` 35 | 36 | As you can see, `SessionManager.cookies` needs a secret key to crypt the sessions' data and keep them secure – you should choose a good key – but also accepts more parameters: 37 | 38 | | parameter | default value | description | 39 | | --- | --- | --- | 40 | | expire | 3600 | the duration in seconds after which the session will expire | 41 | | secure | `False` | tells the manager to allow *https* sessions only | 42 | | samesite | Lax | set `SameSite` option for the cookie | 43 | | domain | | allows to set a specific domain for the cookie | 44 | | cookie\_name | | allows to set a specific name for the cookie | 45 | | cookie\_data | | allows to pass additional cookie data to the manager | 46 | | compression\_level | 0 | allows to set the compression level for the data stored (0 means disabled) | 47 | 48 | Storing sessions on filesystem 49 | ------------------------------ 50 | 51 | *Changed in version 2.1* 52 | 53 | You can store session contents on the server's filesystem using the Emmett's `SessionManager.files` pipe: 54 | 55 | ```python 56 | from emmett import App, session 57 | from emmett.sessions import SessionManager 58 | 59 | app = App(__name__) 60 | app.pipeline = [SessionManager.files()] 61 | 62 | @app.route("/counter") 63 | # previous code 64 | ``` 65 | 66 | As you can see, `SessionManager.files` doesn't require specific parameters, but it accepts these optional ones: 67 | 68 | | parameter | default value | description | 69 | | --- | --- | --- | 70 | | expire | 3600 | the duration in seconds after which the session will expire | 71 | | secure | `False` | tells the manager to allow sessions only on *https* protocol | 72 | | samesite | Lax | set `SameSite` option for the cookie | 73 | | domain | | allows to set a specific domain for the cookie | 74 | | cookie\_name | | allows to set a specific name for the cookie | 75 | | cookie\_data | | allows to pass additional cookie data to the manager | 76 | | filename_template | `'emt_%s.sess'` | allows you to set a specific format for the files created to store the data | 77 | 78 | Storing sessions using redis 79 | ---------------------------- 80 | 81 | *Changed in version 2.1* 82 | 83 | You can store session contents using *redis* – you obviously need the redis package for python – with the Emmett's `SessionManager.redis` pipe: 84 | 85 | ```python 86 | from redis import Redis 87 | from emmett import App, session 88 | from emmett.sessions import SessionManager 89 | 90 | app = App(__name__) 91 | red = Redis(host='127.0.0.1', port=6379) 92 | app.pipeline = [SessionManager.redis(red)] 93 | 94 | @app.route("/counter") 95 | # previous code 96 | ``` 97 | 98 | As you can see `SessionManager.redis` needs a redis connection as first parameter, but as for the cookie manager, it also accepts more parameters: 99 | 100 | | parameter | default | description | 101 | | --- | --- | --- | 102 | | prefix | `'emtsess:'` | the prefix for the redis keys (default set to | 103 | | expire | 3600 | the duration in seconds after which the session will expire | 104 | | secure | `False` | tells the manager to allow sessions only on *https* protocol | 105 | | samesite | Lax | set `SameSite` option for the cookie | 106 | | domain | | allows to set a specific domain for the cookie | 107 | | cookie\_name | | allows to set a specific name for the cookie | 108 | | cookie\_data | | allows to pass additional cookie data to the manager | 109 | 110 | The `expire` parameter tells redis when to auto-delete the unused session: every time the session is updated, the expiration time is reset to the one specified. 111 | -------------------------------------------------------------------------------- /docs/tree.yml: -------------------------------------------------------------------------------- 1 | --- # tree 2 | - foreword 3 | - installation 4 | - quickstart 5 | - tutorial 6 | - app_and_modules 7 | - routing 8 | - templates 9 | - html 10 | - request 11 | - response 12 | - websocket 13 | - pipeline 14 | - sessions 15 | - languages 16 | - orm: 17 | - connecting 18 | - models 19 | - operations 20 | - scopes 21 | - relations 22 | - virtuals 23 | - callbacks 24 | - migrations 25 | - advanced 26 | - validations 27 | - forms 28 | - auth 29 | - mailer 30 | - caching 31 | - services 32 | - testing 33 | - debug_and_logging 34 | - extensions 35 | - cli 36 | - patterns 37 | - deployment 38 | - upgrading 39 | -------------------------------------------------------------------------------- /docs/websocket.md: -------------------------------------------------------------------------------- 1 | Handling websockets 2 | =================== 3 | 4 | *New in version 2.0* 5 | 6 | In the same way we saw for [requests](./request), Emmett also provides facilities to help you dealing with websockets in your application. 7 | 8 | The websocket object 9 | -------------------- 10 | 11 | When a websocket connection comes from a client, Emmett binds useful informations about it within the `websocket` object, which can be accessed just with an import: 12 | 13 | ```python 14 | from emmett import websocket 15 | ``` 16 | 17 | It contains useful information about the current processing socket, in particular: 18 | 19 | | attribute | description | 20 | | --- | --- | 21 | | scheme | could be *ws* or *wss* | 22 | | path | full path of the request | 23 | | host | hostname of the request | 24 | | headers | the headers of the request | 25 | | cookies | the cookies passed with the request | 26 | 27 | Now, let's see how to deal with request variables. 28 | 29 | ### Request variables 30 | 31 | Emmett's `websocket` object also shares the same attributes of `request` when available: 32 | 33 | | attribute | description | 34 | | --- | --- | 35 | | query_params | contains the URL query parameters | 36 | 37 | and also in websockets, this attribute is an `sdict` object so when the URL doesn't contain the query parameter you're trying to look at, this will be `None`, so it's completely safe to call it. It won't raise any exception. 38 | 39 | 40 | Sending and receiving messages 41 | ------------------------------ 42 | 43 | The main difference between request routes and websocket ones is the communication flow. In fact, while in standard routes you just write a return value, with sockets you can receive and send multiple messages within the same connection. 44 | 45 | This is why the `websocket` object in Emmett also has three awaitable methods for this purpose: 46 | 47 | - accept 48 | - receive 49 | - send 50 | 51 | While the `accept` method is implicitly called by the former ones, and is exposed in case you want to specify a specific flow for websockets acceptance, the `receive` and `send` method will be used by Emmett to deal with communications. 52 | 53 | Giving an example, a super simple echo websocket in Emmett will look like this: 54 | 55 | ```python 56 | from emmett import websocket 57 | 58 | @app.websocket() 59 | async def echo(): 60 | while True: 61 | message = await websocket.receive() 62 | await websocket.send(message) 63 | ``` 64 | 65 | Mind that, since a websocket route essentially is a loop, when your code returns Emmett will close the connection. 66 | -------------------------------------------------------------------------------- /emmett/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _internal 2 | from .app import App, AppModule 3 | from .cache import Cache 4 | from .ctx import current 5 | from .datastructures import sdict 6 | from .forms import Form 7 | from .helpers import abort, stream_file 8 | from .html import asis 9 | from .http import redirect 10 | from .locals import T, now, request, response, session, websocket 11 | from .orm import Field 12 | from .pipeline import Injector, Pipe 13 | from .routing.urls import url 14 | -------------------------------------------------------------------------------- /emmett/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.__main__ 4 | ---------------- 5 | 6 | Alias for Emmett CLI. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett.cli import main 13 | 14 | 15 | main(as_module=True) 16 | -------------------------------------------------------------------------------- /emmett/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.7.0" 2 | -------------------------------------------------------------------------------- /emmett/_internal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett._internal 4 | ---------------- 5 | 6 | Provides internally used helpers and objects. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | 10 | Several parts of this code comes from Flask and Werkzeug. 11 | :copyright: (c) 2014 by Armin Ronacher. 12 | 13 | :license: BSD-3-Clause 14 | """ 15 | 16 | from __future__ import annotations 17 | 18 | import datetime 19 | 20 | import pendulum 21 | 22 | 23 | #: monkey patches 24 | def _pendulum_to_datetime(obj): 25 | return datetime.datetime( 26 | obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=obj.tzinfo 27 | ) 28 | 29 | 30 | def _pendulum_to_naive_datetime(obj): 31 | obj = obj.in_timezone("UTC") 32 | return datetime.datetime(obj.year, obj.month, obj.day, obj.hour, obj.minute, obj.second, obj.microsecond) 33 | 34 | 35 | def _pendulum_json(obj): 36 | return obj.for_json() 37 | 38 | 39 | pendulum.DateTime.as_datetime = _pendulum_to_datetime # type: ignore 40 | pendulum.DateTime.as_naive_datetime = _pendulum_to_naive_datetime # type: ignore 41 | pendulum.DateTime.__json__ = _pendulum_json # type: ignore 42 | -------------------------------------------------------------------------------- /emmett/_shortcuts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett._shortcuts 4 | ----------------- 5 | 6 | Some shortcuts 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import hashlib 13 | from uuid import uuid4 14 | 15 | 16 | hashlib_md5 = lambda s: hashlib.md5(bytes(s, "utf8")) 17 | hashlib_sha1 = lambda s: hashlib.sha1(bytes(s, "utf8")) 18 | uuid = lambda: str(uuid4()) 19 | 20 | 21 | def to_bytes(obj, charset="utf8", errors="strict"): 22 | if obj is None: 23 | return None 24 | if isinstance(obj, (bytes, bytearray, memoryview)): 25 | return bytes(obj) 26 | if isinstance(obj, str): 27 | return obj.encode(charset, errors) 28 | raise TypeError("Expected bytes") 29 | 30 | 31 | def to_unicode(obj, charset="utf8", errors="strict"): 32 | if obj is None: 33 | return None 34 | if not isinstance(obj, bytes): 35 | return str(obj) 36 | return obj.decode(charset, errors) 37 | -------------------------------------------------------------------------------- /emmett/asgi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/asgi/__init__.py -------------------------------------------------------------------------------- /emmett/asgi/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.asgi.handlers 4 | -------------------- 5 | 6 | Provides ASGI handlers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from hashlib import md5 15 | from importlib import resources 16 | from typing import Awaitable, Callable 17 | 18 | from emmett_core.http.response import HTTPBytesResponse, HTTPResponse, HTTPStringResponse 19 | from emmett_core.protocols.asgi.handlers import HTTPHandler as _HTTPHandler, RequestCancelled, WSHandler as _WSHandler 20 | from emmett_core.protocols.asgi.typing import Receive, Scope, Send 21 | from emmett_core.utils import cachedprop 22 | 23 | from ..ctx import RequestContext, WSContext, current 24 | from ..debug import debug_handler, smart_traceback 25 | from ..libs.contenttype import contenttype 26 | from .wrappers import Request, Response, Websocket 27 | 28 | 29 | class HTTPHandler(_HTTPHandler): 30 | __slots__ = [] 31 | wrapper_cls = Request 32 | response_cls = Response 33 | 34 | @cachedprop 35 | def error_handler(self) -> Callable[[], Awaitable[str]]: 36 | return self._debug_handler if self.app.debug else self.exception_handler 37 | 38 | async def _static_content(self, content: bytes, content_type: str) -> HTTPBytesResponse: 39 | content_len = str(len(content)) 40 | return HTTPBytesResponse( 41 | 200, 42 | content, 43 | headers={ 44 | "content-type": content_type, 45 | "content-length": content_len, 46 | "last-modified": self._internal_assets_md[1], 47 | "etag": md5(f"{self._internal_assets_md[0]}_{content_len}".encode("utf8")).hexdigest(), 48 | }, 49 | ) 50 | 51 | def _static_handler(self, scope: Scope, receive: Receive, send: Send) -> Awaitable[HTTPResponse]: 52 | path = scope["emt.path"] 53 | #: handle internal assets 54 | if path.startswith("/__emmett__"): 55 | file_name = path[12:] 56 | if not file_name or file_name.endswith(".html"): 57 | return self._http_response(404) 58 | pkg = None 59 | if "/" in file_name: 60 | pkg, file_name = file_name.split("/", 1) 61 | try: 62 | file_contents = resources.read_binary(f"emmett.assets.{pkg}" if pkg else "emmett.assets", file_name) 63 | except FileNotFoundError: 64 | return self._http_response(404) 65 | return self._static_content(file_contents, contenttype(file_name)) 66 | return super()._static_handler(scope, receive, send) 67 | 68 | async def _debug_handler(self) -> str: 69 | current.response.headers._data["content-type"] = "text/html; charset=utf-8" 70 | return debug_handler(smart_traceback(self.app)) 71 | 72 | async def dynamic_handler(self, scope: Scope, receive: Receive, send: Send) -> HTTPResponse: 73 | request = Request( 74 | scope, 75 | receive, 76 | send, 77 | max_content_length=self.app.config.request_max_content_length, 78 | max_multipart_size=self.app.config.request_multipart_max_size, 79 | body_timeout=self.app.config.request_body_timeout, 80 | ) 81 | response = Response(send) 82 | ctx = RequestContext(self.app, request, response) 83 | ctx_token = current._init_(ctx) 84 | try: 85 | http = await self.router.dispatch(request, response) 86 | except HTTPResponse as http_exception: 87 | http = http_exception 88 | #: render error with handlers if in app 89 | error_handler = self.app.error_handlers.get(http.status_code) 90 | if error_handler: 91 | http = HTTPStringResponse( 92 | http.status_code, await error_handler(), headers=response.headers, cookies=response.cookies 93 | ) 94 | except RequestCancelled: 95 | raise 96 | except Exception: 97 | self.app.log.exception("Application exception:") 98 | http = HTTPStringResponse(500, await self.error_handler(), headers=response.headers) 99 | finally: 100 | current._close_(ctx_token) 101 | return http 102 | 103 | async def _exception_handler(self) -> str: 104 | current.response.headers._data["content-type"] = "text/plain" 105 | return "Internal error" 106 | 107 | 108 | class WSHandler(_WSHandler): 109 | __slots__ = [] 110 | wrapper_cls = Websocket 111 | 112 | async def dynamic_handler(self, scope: Scope, send: Send): 113 | ctx = WSContext(self.app, Websocket(scope, scope["emt.input"].get, send)) 114 | ctx_token = current._init_(ctx) 115 | try: 116 | await self.router.dispatch(ctx.websocket) 117 | finally: 118 | if not scope.get("emt._flow_cancel", False) and ctx.websocket._accepted: 119 | await send({"type": "websocket.close", "code": 1000}) 120 | scope["emt._ws_closed"] = True 121 | current._close_(ctx_token) 122 | -------------------------------------------------------------------------------- /emmett/asgi/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.asgi.wrappers 4 | -------------------- 5 | 6 | Provides ASGI request and websocket wrappers 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import pendulum 13 | from emmett_core.protocols.asgi.wrappers import Request as _Request, Response as _Response, Websocket as Websocket 14 | from emmett_core.utils import cachedprop 15 | 16 | from ..wrappers.response import ResponseMixin 17 | 18 | 19 | class Request(_Request): 20 | __slots__ = [] 21 | 22 | @cachedprop 23 | def now(self) -> pendulum.DateTime: 24 | return pendulum.instance(self._now) 25 | 26 | @cachedprop 27 | def now_local(self) -> pendulum.DateTime: 28 | return self.now.in_timezone(pendulum.local_timezone()) # type: ignore 29 | 30 | 31 | class Response(ResponseMixin, _Response): 32 | __slots__ = [] 33 | -------------------------------------------------------------------------------- /emmett/assets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/assets/__init__.py -------------------------------------------------------------------------------- /emmett/assets/debug/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/assets/debug/__init__.py -------------------------------------------------------------------------------- /emmett/assets/debug/shBrushPython.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SyntaxHighlighter 3 | * http://alexgorbatchev.com/SyntaxHighlighter 4 | * 5 | * SyntaxHighlighter is donationware. If you are using it, please donate. 6 | * http://alexgorbatchev.com/SyntaxHighlighter/donate.html 7 | * 8 | * @version 9 | * 3.0.83 (July 02 2010) 10 | * 11 | * @copyright 12 | * Copyright (C) 2004-2010 Alex Gorbatchev. 13 | * 14 | * @license 15 | * Dual licensed under the MIT and GPL licenses. 16 | */ 17 | ;(function() 18 | { 19 | // CommonJS 20 | typeof(require) != 'undefined' ? SyntaxHighlighter = require('shCore').SyntaxHighlighter : null; 21 | 22 | function Brush() 23 | { 24 | // Contributed by Gheorghe Milas and Ahmad Sherif 25 | 26 | var keywords = 'and assert break class continue def del elif else ' + 27 | 'except exec finally for from global if import in is ' + 28 | 'lambda not or pass print raise return try yield while'; 29 | 30 | var funcs = '__import__ abs all any apply basestring bin bool buffer callable ' + 31 | 'chr classmethod cmp coerce compile complex delattr dict dir ' + 32 | 'divmod enumerate eval execfile file filter float format frozenset ' + 33 | 'getattr globals hasattr hash help hex id input int intern ' + 34 | 'isinstance issubclass iter len list locals long map max min next ' + 35 | 'object oct open ord pow print property range raw_input reduce ' + 36 | 'reload repr reversed round set setattr slice sorted staticmethod ' + 37 | 'str sum super tuple type type unichr unicode vars xrange zip'; 38 | 39 | var special = 'None True False self cls class_'; 40 | 41 | this.regexList = [ 42 | { regex: SyntaxHighlighter.regexLib.singleLinePerlComments, css: 'comments' }, 43 | { regex: /^\s*@\w+/gm, css: 'decorator' }, 44 | { regex: /(['\"]{3})([^\1])*?\1/gm, css: 'comments' }, 45 | { regex: /"(?!")(?:\.|\\\"|[^\""\n])*"/gm, css: 'string' }, 46 | { regex: /'(?!')(?:\.|(\\\')|[^\''\n])*'/gm, css: 'string' }, 47 | { regex: /\+|\-|\*|\/|\%|=|==/gm, css: 'keyword' }, 48 | { regex: /\b\d+\.?\w*/g, css: 'value' }, 49 | { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, 50 | { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' }, 51 | { regex: new RegExp(this.getKeywords(special), 'gm'), css: 'color1' } 52 | ]; 53 | 54 | this.forHtmlScript(SyntaxHighlighter.regexLib.aspScriptTags); 55 | }; 56 | 57 | Brush.prototype = new SyntaxHighlighter.Highlighter(); 58 | Brush.aliases = ['py', 'python']; 59 | 60 | SyntaxHighlighter.brushes.Python = Brush; 61 | 62 | // CommonJS 63 | typeof(exports) != 'undefined' ? exports.Brush = Brush : null; 64 | })(); 65 | -------------------------------------------------------------------------------- /emmett/assets/debug/shTheme.css: -------------------------------------------------------------------------------- 1 | .syntaxhighlighter { 2 | background-color: white !important; 3 | } 4 | .syntaxhighlighter .line.alt1 { 5 | background-color: white !important; 6 | } 7 | .syntaxhighlighter .line.alt2 { 8 | background-color: white !important; 9 | } 10 | .syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 { 11 | background-color: #ebdde2 !important; 12 | } 13 | .syntaxhighlighter .line.highlighted.number { 14 | color: black !important; 15 | } 16 | .syntaxhighlighter table caption { 17 | color: black !important; 18 | } 19 | .syntaxhighlighter .gutter { 20 | color: #afafaf !important; 21 | } 22 | .syntaxhighlighter .gutter .line { 23 | border-right: 3px solid orange !important; 24 | } 25 | .syntaxhighlighter .gutter .line.highlighted { 26 | background-color: orange !important; 27 | color: white !important; 28 | } 29 | .syntaxhighlighter.printing .line .content { 30 | border: none !important; 31 | } 32 | .syntaxhighlighter.collapsed { 33 | overflow: visible !important; 34 | } 35 | .syntaxhighlighter.collapsed .toolbar { 36 | color: blue !important; 37 | background: white !important; 38 | border: 1px solid #6ce26c !important; 39 | } 40 | .syntaxhighlighter.collapsed .toolbar a { 41 | color: blue !important; 42 | } 43 | .syntaxhighlighter.collapsed .toolbar a:hover { 44 | color: red !important; 45 | } 46 | .syntaxhighlighter .toolbar { 47 | color: white !important; 48 | background: #6ce26c !important; 49 | border: none !important; 50 | } 51 | .syntaxhighlighter .toolbar a { 52 | color: white !important; 53 | } 54 | .syntaxhighlighter .toolbar a:hover { 55 | color: black !important; 56 | } 57 | .syntaxhighlighter .plain, .syntaxhighlighter .plain a { 58 | color: black !important; 59 | } 60 | .syntaxhighlighter .comments, .syntaxhighlighter .comments a { 61 | color: #008200 !important; 62 | } 63 | .syntaxhighlighter .string, .syntaxhighlighter .string a { 64 | color: orange !important; 65 | } 66 | .syntaxhighlighter .keyword { 67 | color: #18536D !important; 68 | } 69 | .syntaxhighlighter .preprocessor { 70 | color: gray !important; 71 | } 72 | .syntaxhighlighter .variable { 73 | color: #aa7700 !important; 74 | } 75 | .syntaxhighlighter .value { 76 | color: #009900 !important; 77 | } 78 | .syntaxhighlighter .functions { 79 | color: #ff5c1f !important; 80 | } 81 | .syntaxhighlighter .constants { 82 | color: #0066cc !important; 83 | } 84 | .syntaxhighlighter .script { 85 | font-weight: bold !important; 86 | color: #006699 !important; 87 | background-color: none !important; 88 | } 89 | .syntaxhighlighter .color1, .syntaxhighlighter .color1 a { 90 | color: gray !important; 91 | } 92 | .syntaxhighlighter .color2, .syntaxhighlighter .color2 a { 93 | color: #ff1493 !important; 94 | } 95 | .syntaxhighlighter .color3, .syntaxhighlighter .color3 a { 96 | color: red !important; 97 | } 98 | 99 | .syntaxhighlighter .keyword { 100 | font-weight: bold !important; 101 | } 102 | -------------------------------------------------------------------------------- /emmett/assets/debug/view.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | font-family: arial, sans-serif 4 | } 5 | 6 | .header { 7 | background-color: orange; 8 | margin: 0; 9 | padding: 1%; 10 | color: #fff; 11 | font-size: 26px; 12 | font-weight: 600; 13 | } 14 | 15 | .container { 16 | padding: 1%; 17 | } 18 | 19 | h3, h5 { 20 | color: orange; 21 | } 22 | 23 | .traceback { 24 | border: 1px solid #ddd; 25 | margin-top: 5px; 26 | margin-bottom: 5px; 27 | } 28 | 29 | a, a:active, a:hover, a:visited { 30 | text-decoration: none; 31 | color: black; 32 | } 33 | 34 | .frameList { 35 | list-style: none; 36 | } 37 | 38 | .framefile { 39 | margin-top: 45px; 40 | border-bottom: 1px solid #ddd; 41 | } 42 | 43 | .frameth { 44 | border-right: 1px solid #ddd; 45 | padding-right:10px; 46 | } 47 | 48 | .frametd { 49 | padding-left: 10px; 50 | display: block; 51 | width:90%; 52 | word-wrap: break-word; 53 | } 54 | -------------------------------------------------------------------------------- /emmett/assets/debug/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | {{=tb.exception_type}} in {{=tb.frames[-1].rendered_filename}} 15 |
16 |
17 |

{{=tb.exception}}

18 |
19 |
{{=tb.frames[-1].sourceblock}}
20 |
21 |
22 | Application traceback | 23 | Full traceback | 24 | Frames 25 |
26 |
27 |
{{=tb.full_tb}}
28 |
29 |
30 |
{{=tb.app_tb}}
31 |
32 |
33 |
    34 | {{for i, frame in enumerate(tb.frames):}} 35 |
  • 36 | {{is_hidden = (i != len(tb.frames)-1)}} 37 |
    38 |

    39 | File {{=frame.filename}}, in function {{=frame.function_name}} at line {{=frame.lineno}} 40 |

    41 |
    42 |
    Code
    43 |
    {{=frame.sourceblock}}
    44 |
    45 |
    46 |
    Variables
    47 | 48 | 49 | {{for k,v in frame.render_locals.items():}} 50 | 51 | 52 | 53 | 54 | {{pass}} 55 | 56 |
    {{=k}}{{=v}}
    57 |
    58 |
    59 |
  • 60 | {{pass}} 61 |
62 |
63 |
64 | 65 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /emmett/assets/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | helpers.js 3 | ---------- 4 | 5 | Javascript helpers for the emmett project. 6 | 7 | :copyright: (c) 2014-2018 by Marco Stagni 8 | :license: BSD-3-Clause 9 | */ 10 | (function($,undefined) { 11 | 12 | var emmett; 13 | 14 | $.emmett = emmett = { 15 | ajax: function (_url, params, to, _type) { 16 | /*simple ajax function*/ 17 | query = ''; 18 | if(typeof params == "string") { 19 | serialized = $(params).serialize(); 20 | if(serialized) { 21 | query = serialized; 22 | } 23 | } else { 24 | pcs = []; 25 | if(params != null && params != undefined) 26 | for(i = 0; i < params.length; i++) { 27 | q = $("[name=" + params[i] + "]").serialize(); 28 | if(q) { 29 | pcs.push(q); 30 | } 31 | } 32 | if(pcs.length > 0) { 33 | query = pcs.join("&"); 34 | } 35 | } 36 | $.ajax({ 37 | type: _type ? _type : "GET", 38 | url: _url, 39 | data: query, 40 | success: function (msg) { 41 | if(to) { 42 | if(to == ':eval') { 43 | eval(msg); 44 | } 45 | else if(typeof to == 'string') { 46 | $("#" + to).html(msg); 47 | } 48 | else { 49 | to(msg); 50 | } 51 | } 52 | } 53 | }); 54 | }, 55 | 56 | loadComponents : function() { 57 | $('[data-emt_remote]').each(function(index) { 58 | var f = function(obj) { 59 | var g = function(msg) { 60 | obj.html(msg); 61 | }; 62 | return g; 63 | }; 64 | $.emmett.ajax($(this).data("emt_remote"),[], f($(this)), "GET"); 65 | }); 66 | } 67 | } 68 | 69 | $(function() { 70 | emmett.loadComponents(); 71 | }); 72 | 73 | })($); 74 | 75 | ajax = $.emmett.ajax; 76 | -------------------------------------------------------------------------------- /emmett/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.cache 4 | ------------ 5 | 6 | Provides a caching system. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import os 15 | import pickle 16 | import tempfile 17 | import threading 18 | import time 19 | from typing import Any, List, Optional 20 | 21 | from emmett_core.cache import Cache as Cache 22 | from emmett_core.cache.handlers import CacheHandler, RamCache as RamCache, RedisCache as RedisCache 23 | 24 | from ._shortcuts import hashlib_sha1 25 | from .ctx import current 26 | from .libs.portalocker import LockedFile 27 | 28 | 29 | class DiskCache(CacheHandler): 30 | lock = threading.RLock() 31 | _fs_transaction_suffix = ".__mt_cache" 32 | _fs_mode = 0o600 33 | 34 | def __init__(self, cache_dir: str = "cache", threshold: int = 500, default_expire: int = 300): 35 | super().__init__(default_expire=default_expire) 36 | self._threshold = threshold 37 | self._path = os.path.join(current.app.root_path, cache_dir) 38 | #: create required paths if needed 39 | if not os.path.exists(self._path): 40 | os.mkdir(self._path) 41 | 42 | def _get_filename(self, key: str) -> str: 43 | khash = hashlib_sha1(key).hexdigest() 44 | return os.path.join(self._path, khash) 45 | 46 | def _del_file(self, filename: str): 47 | try: 48 | os.remove(filename) 49 | except Exception: 50 | pass 51 | 52 | def _list_dir(self) -> List[str]: 53 | return [ 54 | os.path.join(self._path, fn) 55 | for fn in os.listdir(self._path) 56 | if not fn.endswith(self._fs_transaction_suffix) 57 | ] 58 | 59 | def _prune(self): 60 | with self.lock: 61 | entries = self._list_dir() 62 | if len(entries) > self._threshold: 63 | now = time.time() 64 | try: 65 | for i, fpath in enumerate(entries): 66 | remove = False 67 | f = LockedFile(fpath, "rb") 68 | exp = pickle.load(f.file) 69 | f.close() 70 | remove = exp <= now or i % 3 == 0 71 | if remove: 72 | self._del_file(fpath) 73 | except Exception: 74 | pass 75 | 76 | def get(self, key: str) -> Any: 77 | filename = self._get_filename(key) 78 | try: 79 | with self.lock: 80 | now = time.time() 81 | f = LockedFile(filename, "rb") 82 | exp = pickle.load(f.file) 83 | if exp < now: 84 | f.close() 85 | return None 86 | val = pickle.load(f.file) 87 | f.close() 88 | except Exception: 89 | return None 90 | return val 91 | 92 | @CacheHandler._convert_duration_ 93 | def set(self, key: str, value: Any, **kwargs): 94 | filename = self._get_filename(key) 95 | filepath = os.path.join(self._path, filename) 96 | with self.lock: 97 | self._prune() 98 | if os.path.exists(filepath): 99 | self._del_file(filepath) 100 | try: 101 | fd, tmp = tempfile.mkstemp(suffix=self._fs_transaction_suffix, dir=self._path) 102 | with os.fdopen(fd, "wb") as f: 103 | pickle.dump(kwargs["expiration"], f, 1) 104 | pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) 105 | os.rename(tmp, filename) 106 | os.chmod(filename, self._fs_mode) 107 | except Exception: 108 | pass 109 | 110 | def clear(self, key: Optional[str] = None): 111 | with self.lock: 112 | if key is not None: 113 | filename = self._get_filename(key) 114 | try: 115 | os.remove(filename) 116 | return 117 | except Exception: 118 | return 119 | for name in self._list_dir(): 120 | self._del_file(name) 121 | -------------------------------------------------------------------------------- /emmett/ctx.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.ctx 4 | ---------- 5 | 6 | Provides the current object. 7 | Used by application to deal with request related objects. 8 | 9 | :copyright: 2014 Giovanni Barillari 10 | :license: BSD-3-Clause 11 | """ 12 | 13 | from datetime import datetime 14 | 15 | import pendulum 16 | from emmett_core.ctx import ( 17 | Context as _Context, 18 | Current as _Current, 19 | RequestContext as _RequestContext, 20 | WSContext as _WsContext, 21 | _ctxv, 22 | ) 23 | from emmett_core.utils import cachedprop 24 | 25 | 26 | class Context(_Context): 27 | __slots__ = [] 28 | 29 | @property 30 | def now(self): 31 | return pendulum.instance(datetime.utcnow()) 32 | 33 | 34 | class RequestContext(_RequestContext): 35 | __slots__ = [] 36 | 37 | @cachedprop 38 | def language(self): 39 | return self.request.accept_language.best_match(list(self.app.translator._langmap)) 40 | 41 | 42 | class WSContext(_WsContext): 43 | __slots__ = [] 44 | 45 | @property 46 | def now(self): 47 | return pendulum.instance(datetime.utcnow()) 48 | 49 | @cachedprop 50 | def language(self): 51 | return self.websocket.accept_language.best_match(list(self.app.translator._langmap)) 52 | 53 | 54 | class Current(_Current): 55 | __slots__ = [] 56 | 57 | def __init__(self): 58 | _ctxv.set(Context()) 59 | 60 | @property 61 | def T(self): 62 | return self.ctx.app.translator 63 | 64 | 65 | current = Current() 66 | -------------------------------------------------------------------------------- /emmett/datastructures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.datastructures 4 | --------------------- 5 | 6 | Provide some useful data structures. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.datastructures import sdict as sdict 13 | 14 | 15 | class OrderedSet(set): 16 | def __init__(self, d=None): 17 | set.__init__(self) 18 | self._list = [] 19 | if d is not None: 20 | self._list = _unique_list(d) 21 | set.update(self, self._list) 22 | else: 23 | self._list = [] 24 | 25 | def add(self, element): 26 | if element not in self: 27 | self._list.append(element) 28 | set.add(self, element) 29 | 30 | def remove(self, element): 31 | set.remove(self, element) 32 | self._list.remove(element) 33 | 34 | def insert(self, pos, element): 35 | if element not in self: 36 | self._list.insert(pos, element) 37 | set.add(self, element) 38 | 39 | def discard(self, element): 40 | if element in self: 41 | self._list.remove(element) 42 | set.remove(self, element) 43 | 44 | def clear(self): 45 | set.clear(self) 46 | self._list = [] 47 | 48 | def __getitem__(self, key): 49 | return self._list[key] 50 | 51 | def __iter__(self): 52 | return iter(self._list) 53 | 54 | def __add__(self, other): 55 | return self.union(other) 56 | 57 | def __repr__(self): 58 | return "%s(%r)" % (self.__class__.__name__, self._list) 59 | 60 | __str__ = __repr__ 61 | 62 | def update(self, iterable): 63 | for e in iterable: 64 | if e not in self: 65 | self._list.append(e) 66 | set.add(self, e) 67 | return self 68 | 69 | __ior__ = update 70 | 71 | def union(self, other): 72 | result = self.__class__(self) 73 | result.update(other) 74 | return result 75 | 76 | __or__ = union 77 | 78 | def intersection(self, other): 79 | other = set(other) 80 | return self.__class__(a for a in self if a in other) 81 | 82 | __and__ = intersection 83 | 84 | def symmetric_difference(self, other): 85 | other = set(other) 86 | result = self.__class__(a for a in self if a not in other) 87 | result.update(a for a in other if a not in self) 88 | return result 89 | 90 | __xor__ = symmetric_difference 91 | 92 | def difference(self, other): 93 | other = set(other) 94 | return self.__class__(a for a in self if a not in other) 95 | 96 | __sub__ = difference 97 | 98 | def intersection_update(self, other): 99 | other = set(other) 100 | set.intersection_update(self, other) 101 | self._list = [a for a in self._list if a in other] 102 | return self 103 | 104 | __iand__ = intersection_update 105 | 106 | def symmetric_difference_update(self, other): 107 | set.symmetric_difference_update(self, other) 108 | self._list = [a for a in self._list if a in self] 109 | self._list += [a for a in other._list if a in self] 110 | return self 111 | 112 | __ixor__ = symmetric_difference_update 113 | 114 | def difference_update(self, other): 115 | set.difference_update(self, other) 116 | self._list = [a for a in self._list if a in self] 117 | return self 118 | 119 | __isub__ = difference_update 120 | 121 | 122 | def _unique_list(seq, hashfunc=None): 123 | seen = set() 124 | seen_add = seen.add 125 | if not hashfunc: 126 | return [x for x in seq if x not in seen and not seen_add(x)] 127 | return [x for x in seq if hashfunc(x) not in seen and not seen_add(hashfunc(x))] 128 | -------------------------------------------------------------------------------- /emmett/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.extensions 4 | ----------------- 5 | 6 | Provides base classes to create extensions. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from enum import Enum 15 | 16 | from emmett_core.extensions import Extension as Extension, listen_signal as listen_signal 17 | 18 | 19 | class Signals(str, Enum): 20 | __str__ = lambda v: v.value 21 | 22 | after_database = "after_database" 23 | after_loop = "after_loop" 24 | after_route = "after_route" 25 | before_database = "before_database" 26 | before_route = "before_route" 27 | before_routes = "before_routes" 28 | -------------------------------------------------------------------------------- /emmett/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.helpers 4 | -------------- 5 | 6 | Provides helping methods for applications. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from typing import Any, List, Optional, Tuple, Union 13 | 14 | from emmett_core._internal import deprecated 15 | from emmett_core.http.helpers import abort as _abort 16 | 17 | from .ctx import current 18 | from .html import HtmlTag, tag 19 | 20 | 21 | def abort(code: int, body: str = ""): 22 | _abort(current, code, body) 23 | 24 | 25 | @deprecated("stream_file", "Response.wrap_file") 26 | def stream_file(path: str): 27 | raise current.response.wrap_file(path) 28 | 29 | 30 | @deprecated("stream_dbfile", "Response.wrap_dbfile") 31 | def stream_dbfile(db: Any, name: str): 32 | raise current.response.wrap_dbfile(db, name) 33 | 34 | 35 | def flash(message: str, category: str = "message"): 36 | #: Flashes a message to the next request. 37 | if current.session._flashes is None: 38 | current.session._flashes = [] 39 | current.session._flashes.append((category, message)) 40 | 41 | 42 | def get_flashed_messages( 43 | with_categories: bool = False, category_filter: Union[str, List[str]] = [] 44 | ) -> Union[List[str], Tuple[str, str]]: 45 | #: Pulls flashed messages from the session and returns them. 46 | # By default just the messages are returned, but when `with_categories` 47 | # is set to `True`, the return value will be a list of tuples in the 48 | # form `(category, message)` instead. 49 | if not isinstance(category_filter, list): 50 | category_filter = [category_filter] 51 | try: 52 | flashes = list(current.session._flashes or []) 53 | if category_filter: 54 | flashes = list(filter(lambda f: f[0] in category_filter, flashes)) 55 | for el in flashes: 56 | current.session._flashes.remove(el) 57 | if not with_categories: 58 | return [x[1] for x in flashes] 59 | except Exception: 60 | flashes = [] 61 | return flashes 62 | 63 | 64 | def load_component(url: str, target: Optional[str] = None, content: str = "loading...") -> HtmlTag: 65 | attr = {} 66 | if target: 67 | attr["_id"] = target 68 | attr["_data-emt_remote"] = url 69 | return tag.div(content, **attr) 70 | -------------------------------------------------------------------------------- /emmett/html.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.html 4 | ----------- 5 | 6 | Provides html generation classes. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import re 13 | from functools import reduce 14 | 15 | from emmett_core.html import ( 16 | MetaHtmlTag as _MetaHtmlTag, 17 | TagStack, 18 | TreeHtmlTag, 19 | _to_str, 20 | cat as cat, 21 | htmlescape as htmlescape, 22 | ) 23 | 24 | 25 | __all__ = ["tag", "cat", "asis"] 26 | 27 | _re_tag = re.compile(r"^([\w\-\:]+)") 28 | _re_id = re.compile(r"#([\w\-]+)") 29 | _re_class = re.compile(r"\.([\w\-]+)") 30 | _re_attr = re.compile(r"\[([\w\-\:]+)=(.*?)\]") 31 | 32 | 33 | class HtmlTag(TreeHtmlTag): 34 | __slots__ = [] 35 | 36 | def __call__(self, *components, **attributes): 37 | # legacy "data" attribute 38 | if _data := attributes.pop("data", None): 39 | attributes["_data"] = _data 40 | return super().__call__(*components, **attributes) 41 | 42 | def find(self, expr): 43 | union = lambda a, b: a.union(b) 44 | if "," in expr: 45 | tags = reduce(union, [self.find(x.strip()) for x in expr.split(",")], set()) 46 | elif " " in expr: 47 | tags = [self] 48 | for k, item in enumerate(expr.split()): 49 | if k > 0: 50 | children = [{c for c in tag if isinstance(c, self.__class__)} for tag in tags] 51 | tags = reduce(union, children) 52 | tags = reduce(union, [tag.find(item) for tag in tags], set()) 53 | else: 54 | tags = reduce(union, [c.find(expr) for c in self if isinstance(c, self.__class__)], set()) 55 | tag = _re_tag.match(expr) 56 | id = _re_id.match(expr) 57 | _class = _re_class.match(expr) 58 | attr = _re_attr.match(expr) 59 | if ( 60 | (tag is None or self.name == tag.group(1)) 61 | and (id is None or self["_id"] == id.group(1)) 62 | and (_class is None or _class.group(1) in (self["_class"] or "").split()) 63 | and (attr is None or self["_" + attr.group(1)] == attr.group(2)) 64 | ): 65 | tags.add(self) 66 | return tags 67 | 68 | 69 | class MetaHtmlTag(_MetaHtmlTag): 70 | __slots__ = [] 71 | _tag_cls = HtmlTag 72 | 73 | 74 | class asis(HtmlTag): 75 | __slots__ = [] 76 | 77 | def __init__(self, val): 78 | self.name = val 79 | 80 | def __html__(self): 81 | return _to_str(self.name) 82 | 83 | 84 | tag = MetaHtmlTag(TagStack()) 85 | -------------------------------------------------------------------------------- /emmett/http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.http 4 | ----------- 5 | 6 | Provides the HTTP interfaces. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from emmett_core.http.helpers import redirect as _redirect 15 | from emmett_core.http.response import ( 16 | HTTPAsyncIterResponse as HTTPAsyncIter, 17 | HTTPBytesResponse as HTTPBytes, 18 | HTTPFileResponse as HTTPFile, 19 | HTTPIOResponse as HTTPIO, 20 | HTTPIterResponse as HTTPIter, 21 | HTTPResponse as HTTPResponse, 22 | HTTPStringResponse as HTTPStringResponse, 23 | ) 24 | 25 | from .ctx import current 26 | 27 | 28 | HTTP = HTTPStringResponse 29 | 30 | status_codes = { 31 | 100: "100 CONTINUE", 32 | 101: "101 SWITCHING PROTOCOLS", 33 | 200: "200 OK", 34 | 201: "201 CREATED", 35 | 202: "202 ACCEPTED", 36 | 203: "203 NON-AUTHORITATIVE INFORMATION", 37 | 204: "204 NO CONTENT", 38 | 205: "205 RESET CONTENT", 39 | 206: "206 PARTIAL CONTENT", 40 | 207: "207 MULTI-STATUS", 41 | 300: "300 MULTIPLE CHOICES", 42 | 301: "301 MOVED PERMANENTLY", 43 | 302: "302 FOUND", 44 | 303: "303 SEE OTHER", 45 | 304: "304 NOT MODIFIED", 46 | 305: "305 USE PROXY", 47 | 307: "307 TEMPORARY REDIRECT", 48 | 400: "400 BAD REQUEST", 49 | 401: "401 UNAUTHORIZED", 50 | 403: "403 FORBIDDEN", 51 | 404: "404 NOT FOUND", 52 | 405: "405 METHOD NOT ALLOWED", 53 | 406: "406 NOT ACCEPTABLE", 54 | 407: "407 PROXY AUTHENTICATION REQUIRED", 55 | 408: "408 REQUEST TIMEOUT", 56 | 409: "409 CONFLICT", 57 | 410: "410 GONE", 58 | 411: "411 LENGTH REQUIRED", 59 | 412: "412 PRECONDITION FAILED", 60 | 413: "413 REQUEST ENTITY TOO LARGE", 61 | 414: "414 REQUEST-URI TOO LONG", 62 | 415: "415 UNSUPPORTED MEDIA TYPE", 63 | 416: "416 REQUESTED RANGE NOT SATISFIABLE", 64 | 417: "417 EXPECTATION FAILED", 65 | 422: "422 UNPROCESSABLE ENTITY", 66 | 500: "500 INTERNAL SERVER ERROR", 67 | 501: "501 NOT IMPLEMENTED", 68 | 502: "502 BAD GATEWAY", 69 | 503: "503 SERVICE UNAVAILABLE", 70 | 504: "504 GATEWAY TIMEOUT", 71 | 505: "505 HTTP VERSION NOT SUPPORTED", 72 | } 73 | 74 | 75 | def redirect(location: str, status_code: int = 303): 76 | _redirect(current, location, status_code) 77 | -------------------------------------------------------------------------------- /emmett/language/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/language/__init__.py -------------------------------------------------------------------------------- /emmett/language/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.language.helpers 4 | ----------------------- 5 | 6 | Translation helpers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import re 13 | 14 | from emmett_core.http.headers import Accept 15 | from severus.datastructures import Tstr as _Tstr 16 | 17 | 18 | class Tstr(_Tstr): 19 | __slots__ = [] 20 | 21 | def __getstate__(self): 22 | return {"text": self.text, "lang": self.lang, "args": self.args, "kwargs": self.kwargs} 23 | 24 | def __setstate__(self, state): 25 | self.text = state["text"] 26 | self.lang = state["lang"] 27 | self.args = state["args"] 28 | self.kwargs = state["kwargs"] 29 | 30 | def __getattr__(self, name): 31 | return getattr(str(self), name) 32 | 33 | def __json__(self): 34 | return str(self) 35 | 36 | 37 | class LanguageAccept(Accept): 38 | regex_locale_delim = re.compile(r"[_-]") 39 | 40 | def _value_matches(self, value, item): 41 | def _normalize(language): 42 | return self.regex_locale_delim.split(language.lower())[0] 43 | 44 | return item == "*" or _normalize(value) == _normalize(item) 45 | -------------------------------------------------------------------------------- /emmett/language/translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.language.translator 4 | -------------------------- 5 | 6 | Severus translator implementation for Emmett. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from typing import Optional 15 | 16 | from severus.ctx import set_context 17 | from severus.translator import Translator as _Translator 18 | 19 | from ..ctx import current 20 | 21 | 22 | class Translator(_Translator): 23 | __slots__ = [] 24 | 25 | def __init__(self, *args, **kwargs): 26 | super().__init__(*args, **kwargs) 27 | set_context(self) 28 | 29 | def _update_config(self, default_language: str): 30 | self._default_language = default_language 31 | self._langmap.clear() 32 | self._languages.clear() 33 | self._build_languages() 34 | 35 | def _get_best_language(self, lang: Optional[str] = None) -> str: 36 | return self._langmap.get(lang or current.language, self._default_language) 37 | -------------------------------------------------------------------------------- /emmett/libs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/libs/__init__.py -------------------------------------------------------------------------------- /emmett/libs/portalocker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # portalocker.py 4 | # Cross-platform (posix/nt) API for flock-style file locking. 5 | # Requires python 1.5.2 or better. 6 | 7 | """ 8 | Cross-platform (posix/nt) API for flock-style file locking. 9 | 10 | Synopsis: 11 | 12 | import portalocker 13 | file = open(\"somefile\", \"r+\") 14 | portalocker.lock(file, portalocker.LOCK_EX) 15 | file.seek(12) 16 | file.write(\"foo\") 17 | file.close() 18 | 19 | If you know what you're doing, you may choose to 20 | 21 | portalocker.unlock(file) 22 | 23 | before closing the file, but why? 24 | 25 | Methods: 26 | 27 | lock( file, flags ) 28 | unlock( file ) 29 | 30 | Constants: 31 | 32 | LOCK_EX 33 | LOCK_SH 34 | LOCK_NB 35 | 36 | I learned the win32 technique for locking files from sample code 37 | provided by John Nielsen in the documentation 38 | that accompanies the win32 modules. 39 | 40 | Author: Jonathan Feinberg 41 | Version: $Id: portalocker.py,v 1.3 2001/05/29 18:47:55 Administrator Exp $ 42 | """ 43 | 44 | os_locking = None 45 | try: 46 | os_locking = "gae" 47 | except Exception: 48 | try: 49 | import fcntl 50 | 51 | os_locking = "posix" 52 | except Exception: 53 | try: 54 | import pywintypes 55 | import win32con 56 | import win32file 57 | 58 | os_locking = "windows" 59 | except Exception: 60 | pass 61 | 62 | if os_locking == "windows": 63 | LOCK_EX = win32con.LOCKFILE_EXCLUSIVE_LOCK 64 | LOCK_SH = 0 # the default 65 | LOCK_NB = win32con.LOCKFILE_FAIL_IMMEDIATELY 66 | 67 | # is there any reason not to reuse the following structure? 68 | 69 | __overlapped = pywintypes.OVERLAPPED() 70 | 71 | def lock(file, flags): 72 | hfile = win32file._get_osfhandle(file.fileno()) 73 | win32file.LockFileEx(hfile, flags, 0, 0x7FFF0000, __overlapped) 74 | 75 | def unlock(file): 76 | hfile = win32file._get_osfhandle(file.fileno()) 77 | win32file.UnlockFileEx(hfile, 0, 0x7FFF0000, __overlapped) 78 | 79 | 80 | elif os_locking == "posix": 81 | LOCK_EX = fcntl.LOCK_EX 82 | LOCK_SH = fcntl.LOCK_SH 83 | LOCK_NB = fcntl.LOCK_NB 84 | 85 | def lock(file, flags): 86 | fcntl.flock(file.fileno(), flags) 87 | 88 | def unlock(file): 89 | fcntl.flock(file.fileno(), fcntl.LOCK_UN) 90 | 91 | 92 | else: 93 | # if platform.system() == 'Windows': 94 | # logger.error('no file locking, you must install the win32 extensions from: http://sourceforge.net/projects/pywin32/files/') 95 | # elif os_locking != 'gae': 96 | # logger.debug('no file locking, this will cause problems') 97 | 98 | LOCK_EX = None 99 | LOCK_SH = None 100 | LOCK_NB = None 101 | 102 | def lock(file, flags): 103 | pass 104 | 105 | def unlock(file): 106 | pass 107 | 108 | 109 | class LockedFile(object): 110 | def __init__(self, filename, mode="rb"): 111 | self.filename = filename 112 | self.mode = mode 113 | self.file = None 114 | if "r" in mode: 115 | kwargs = {"encoding": "utf8"} if "b" not in mode else {} 116 | self.file = open(filename, mode, **kwargs) 117 | lock(self.file, LOCK_SH) 118 | elif "w" in mode or "a" in mode: 119 | self.file = open(filename, mode.replace("w", "a")) 120 | lock(self.file, LOCK_EX) 121 | if "a" not in mode: 122 | self.file.seek(0) 123 | self.file.truncate() 124 | else: 125 | raise RuntimeError("invalid LockedFile(...,mode)") 126 | 127 | def read(self, size=None): 128 | return self.file.read() if size is None else self.file.read(size) 129 | 130 | def readline(self): 131 | return self.file.readline() 132 | 133 | def readlines(self): 134 | return self.file.readlines() 135 | 136 | def write(self, data): 137 | self.file.write(data) 138 | self.file.flush() 139 | 140 | def close(self): 141 | if self.file is not None: 142 | unlock(self.file) 143 | self.file.close() 144 | self.file = None 145 | 146 | def __del__(self): 147 | if self.file is not None: 148 | self.close() 149 | 150 | 151 | def read_locked(filename): 152 | fp = LockedFile(filename, "r") 153 | data = fp.read() 154 | fp.close() 155 | return data 156 | 157 | 158 | def write_locked(filename, data): 159 | fp = LockedFile(filename, "wb") 160 | data = fp.write(data) 161 | fp.close() 162 | -------------------------------------------------------------------------------- /emmett/locals.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.locals 4 | ------------- 5 | 6 | Provides shortcuts to `current` object. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from typing import Optional, cast 13 | 14 | from emmett_core._internal import ContextVarProxy as _VProxy, ObjectProxy as _OProxy 15 | from pendulum import DateTime 16 | 17 | from .ctx import _ctxv, current 18 | from .datastructures import sdict 19 | from .language.translator import Translator 20 | from .wrappers.request import Request 21 | from .wrappers.response import Response 22 | from .wrappers.websocket import Websocket 23 | 24 | 25 | request = cast(Request, _VProxy[Request](_ctxv, "request")) 26 | response = cast(Response, _VProxy[Response](_ctxv, "response")) 27 | session = cast(Optional[sdict], _VProxy[Optional[sdict]](_ctxv, "session")) 28 | websocket = cast(Websocket, _VProxy[Websocket](_ctxv, "websocket")) 29 | T = cast(Translator, _OProxy[Translator](current, "T")) 30 | 31 | 32 | def now() -> DateTime: 33 | return current.now 34 | -------------------------------------------------------------------------------- /emmett/orm/__init__.py: -------------------------------------------------------------------------------- 1 | from . import _patches 2 | from .adapters import adapters as adapters_registry 3 | from .apis import ( 4 | after_commit, 5 | after_delete, 6 | after_destroy, 7 | after_insert, 8 | after_save, 9 | after_update, 10 | before_commit, 11 | before_delete, 12 | before_destroy, 13 | before_insert, 14 | before_save, 15 | before_update, 16 | belongs_to, 17 | compute, 18 | has_many, 19 | has_one, 20 | refers_to, 21 | rowattr, 22 | rowmethod, 23 | scope, 24 | ) 25 | from .base import Database 26 | from .models import Model 27 | from .objects import Field, TransactionOps 28 | -------------------------------------------------------------------------------- /emmett/orm/_patches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm._patches 4 | ------------------- 5 | 6 | Provides pyDAL patches. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.utils import cachedprop 13 | from pydal.adapters.base import BaseAdapter 14 | from pydal.connection import ConnectionPool 15 | from pydal.helpers.classes import ConnectionConfigurationMixin 16 | from pydal.helpers.serializers import Serializers as _Serializers 17 | 18 | from ..serializers import Serializers 19 | from .adapters import ( 20 | _begin, 21 | _in_transaction, 22 | _initialize, 23 | _pop_transaction, 24 | _push_transaction, 25 | _top_transaction, 26 | _transaction_depth, 27 | ) 28 | from .connection import ( 29 | PooledConnectionManager, 30 | _close_loop, 31 | _close_sync, 32 | _connect_and_configure, 33 | _connect_loop, 34 | _connect_sync, 35 | _connection_getter, 36 | _connection_init, 37 | _connection_setter, 38 | _cursors_getter, 39 | ) 40 | 41 | 42 | def _patch_adapter_cls(): 43 | BaseAdapter._initialize_ = _initialize 44 | BaseAdapter.in_transaction = _in_transaction 45 | BaseAdapter.push_transaction = _push_transaction 46 | BaseAdapter.pop_transaction = _pop_transaction 47 | BaseAdapter.transaction_depth = _transaction_depth 48 | BaseAdapter.top_transaction = _top_transaction 49 | BaseAdapter._connection_manager_cls = PooledConnectionManager 50 | BaseAdapter.begin = _begin 51 | 52 | 53 | def _patch_adapter_connection(): 54 | ConnectionPool.__init__ = _connection_init 55 | ConnectionPool.reconnect = _connect_sync 56 | ConnectionPool.reconnect_loop = _connect_loop 57 | ConnectionPool.close = _close_sync 58 | ConnectionPool.close_loop = _close_loop 59 | ConnectionPool.connection = property(_connection_getter, _connection_setter) 60 | ConnectionPool.cursors = property(_cursors_getter) 61 | ConnectionConfigurationMixin._reconnect_and_configure = _connect_and_configure 62 | 63 | 64 | def _patch_serializers(): 65 | _Serializers.json = cachedprop(lambda _: Serializers.get_for("json"), name="json") 66 | 67 | 68 | _patch_adapter_cls() 69 | _patch_adapter_connection() 70 | _patch_serializers() 71 | -------------------------------------------------------------------------------- /emmett/orm/apis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.apis 4 | --------------- 5 | 6 | Provides ORM apis. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from collections import OrderedDict 13 | from typing import List 14 | 15 | from .errors import MissingFieldsForCompute 16 | from .helpers import Callback, Reference 17 | 18 | 19 | class belongs_to(Reference): 20 | _references_ = OrderedDict() 21 | 22 | @property 23 | def refobj(self): 24 | return belongs_to._references_ 25 | 26 | 27 | class refers_to(Reference): 28 | _references_ = OrderedDict() 29 | 30 | @property 31 | def refobj(self): 32 | return refers_to._references_ 33 | 34 | 35 | class has_one(Reference): 36 | _references_ = OrderedDict() 37 | 38 | @property 39 | def refobj(self): 40 | return has_one._references_ 41 | 42 | 43 | class has_many(Reference): 44 | _references_ = OrderedDict() 45 | 46 | @property 47 | def refobj(self): 48 | return has_many._references_ 49 | 50 | 51 | class compute(object): 52 | _inst_count_ = 0 53 | 54 | def __init__(self, field_name: str, watch: List[str] = []): 55 | self.field_name = field_name 56 | self.watch_fields = set(watch) 57 | self._inst_count_ = compute._inst_count_ 58 | compute._inst_count_ += 1 59 | 60 | def __call__(self, f): 61 | self.f = f 62 | return self 63 | 64 | def compute(self, model, op_row): 65 | if self.watch_fields: 66 | row_keyset = set(op_row.keys()) 67 | if row_keyset & self.watch_fields: 68 | if not self.watch_fields.issubset(row_keyset): 69 | raise MissingFieldsForCompute( 70 | f"Compute field '{self.field_name}' missing required " 71 | f"({','.join(self.watch_fields - row_keyset)})" 72 | ) 73 | else: 74 | return 75 | return self.f(model, op_row) 76 | 77 | 78 | class rowattr(object): 79 | _inst_count_ = 0 80 | 81 | def __init__(self, field_name): 82 | self.field_name = field_name 83 | self._inst_count_ = rowattr._inst_count_ 84 | rowattr._inst_count_ += 1 85 | 86 | def __call__(self, f): 87 | self.f = f 88 | return self 89 | 90 | 91 | class rowmethod(rowattr): 92 | pass 93 | 94 | 95 | def before_insert(f): 96 | return Callback(f, "_before_insert") 97 | 98 | 99 | def after_insert(f): 100 | return Callback(f, "_after_insert") 101 | 102 | 103 | def before_update(f): 104 | return Callback(f, "_before_update") 105 | 106 | 107 | def after_update(f): 108 | return Callback(f, "_after_update") 109 | 110 | 111 | def before_delete(f): 112 | return Callback(f, "_before_delete") 113 | 114 | 115 | def after_delete(f): 116 | return Callback(f, "_after_delete") 117 | 118 | 119 | def before_save(f): 120 | return Callback(f, "_before_save") 121 | 122 | 123 | def after_save(f): 124 | return Callback(f, "_after_save") 125 | 126 | 127 | def before_destroy(f): 128 | return Callback(f, "_before_destroy") 129 | 130 | 131 | def after_destroy(f): 132 | return Callback(f, "_after_destroy") 133 | 134 | 135 | def before_commit(f): 136 | return Callback(f, "_before_commit") 137 | 138 | 139 | def after_commit(f): 140 | return Callback(f, "_after_commit") 141 | 142 | 143 | def _commit_callback_op(kind, op): 144 | def _deco(f): 145 | return Callback(f, f"_{kind}_commit_{op}") 146 | 147 | return _deco 148 | 149 | 150 | before_commit.operation = lambda op: _commit_callback_op("before", op) 151 | after_commit.operation = lambda op: _commit_callback_op("after", op) 152 | 153 | 154 | class scope(object): 155 | def __init__(self, name): 156 | self.name = name 157 | 158 | def __call__(self, f): 159 | self.f = f 160 | return self 161 | -------------------------------------------------------------------------------- /emmett/orm/engines/__init__.py: -------------------------------------------------------------------------------- 1 | from pydal.adapters import adapters 2 | 3 | from . import postgres, sqlite 4 | -------------------------------------------------------------------------------- /emmett/orm/engines/sqlite.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.engines.sqlite 4 | ------------------------- 5 | 6 | Provides ORM SQLite engine specific features. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from pydal.adapters.sqlite import SQLite as _SQLite 13 | 14 | from . import adapters 15 | 16 | 17 | @adapters.register_for("sqlite", "sqlite:memory") 18 | class SQLite(_SQLite): 19 | def _initialize_(self, do_connect): 20 | super()._initialize_(do_connect) 21 | self.driver_args["isolation_level"] = None 22 | 23 | def begin(self, lock_type=None): 24 | statement = "BEGIN %s;" % lock_type if lock_type else "BEGIN;" 25 | self.execute(statement) 26 | 27 | def delete(self, table, query): 28 | deleted = [x[table._id.name] for x in self.db(query).select(table._id)] if table._id else [] 29 | counter = super(_SQLite, self).delete(table, query) 30 | if table._id and counter: 31 | for field in table._referenced_by: 32 | if field.type == "reference " + table._dalname and field.ondelete == "CASCADE": 33 | self.db(field.belongs(deleted)).delete() 34 | return counter 35 | -------------------------------------------------------------------------------- /emmett/orm/errors.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.errors 4 | ----------------- 5 | 6 | Provides some error wrappers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | 13 | class MaxConnectionsExceeded(RuntimeError): 14 | def __init__(self): 15 | super().__init__("Exceeded maximum connections") 16 | 17 | 18 | class MissingFieldsForCompute(RuntimeError): ... 19 | 20 | 21 | class SaveException(RuntimeError): ... 22 | 23 | 24 | class InsertFailureOnSave(SaveException): ... 25 | 26 | 27 | class UpdateFailureOnSave(SaveException): ... 28 | 29 | 30 | class DestroyException(RuntimeError): ... 31 | 32 | 33 | class ValidationError(RuntimeError): ... 34 | -------------------------------------------------------------------------------- /emmett/orm/geo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.geo 4 | -------------- 5 | 6 | Provides geographic facilities. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from .helpers import GeoFieldWrapper 13 | 14 | 15 | def Point(x, y): 16 | return GeoFieldWrapper("POINT(%f %f)" % (x, y)) 17 | 18 | 19 | def Line(*coordinates): 20 | return GeoFieldWrapper("LINESTRING(%s)" % ",".join("%f %f" % point for point in coordinates)) 21 | 22 | 23 | def Polygon(*coordinates_groups): 24 | try: 25 | if not isinstance(coordinates_groups[0][0], (tuple, list)): 26 | coordinates_groups = (coordinates_groups,) 27 | except Exception: 28 | pass 29 | return GeoFieldWrapper( 30 | "POLYGON(%s)" 31 | % (",".join(["(%s)" % ",".join("%f %f" % point for point in group) for group in coordinates_groups])) 32 | ) 33 | 34 | 35 | def MultiPoint(*points): 36 | return GeoFieldWrapper("MULTIPOINT(%s)" % (",".join(["(%f %f)" % point for point in points]))) 37 | 38 | 39 | def MultiLine(*lines): 40 | return GeoFieldWrapper( 41 | "MULTILINESTRING(%s)" % (",".join(["(%s)" % ",".join("%f %f" % point for point in line) for line in lines])) 42 | ) 43 | 44 | 45 | def MultiPolygon(*polygons): 46 | return GeoFieldWrapper( 47 | "MULTIPOLYGON(%s)" 48 | % ( 49 | ",".join( 50 | [ 51 | "(%s)" % (",".join(["(%s)" % ",".join("%f %f" % point for point in group) for group in polygon])) 52 | for polygon in polygons 53 | ] 54 | ) 55 | ) 56 | ) 57 | -------------------------------------------------------------------------------- /emmett/orm/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import Column, Migration 2 | from .operations import * 3 | -------------------------------------------------------------------------------- /emmett/orm/migrations/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.migrations.base 4 | -------------------------- 5 | 6 | Provides base migrations objects. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from typing import TYPE_CHECKING, Any, Callable, Dict, Type 15 | 16 | from ...datastructures import sdict 17 | from .. import Database, Field, Model 18 | from .engine import Engine, MetaEngine 19 | from .helpers import WrappedOperation, _feasible_as_dbms_default 20 | 21 | 22 | if TYPE_CHECKING: 23 | from .operations import Operation 24 | 25 | 26 | class Schema(Model): 27 | tablename = "emmett_schema" 28 | version = Field() 29 | 30 | 31 | class Migration: 32 | _registered_ops_: Dict[str, Type[Operation]] = {} 33 | skip_on_compare: bool = False 34 | 35 | @classmethod 36 | def register_operation(cls, name: str) -> Callable[[Type[Operation]], Type[Operation]]: 37 | def wrap(op_cls: Type[Operation]) -> Type[Operation]: 38 | cls._registered_ops_[name] = op_cls 39 | return op_cls 40 | 41 | return wrap 42 | 43 | def __init__(self, app: Any, db: Database, is_meta: bool = False): 44 | self.db = db 45 | if is_meta: 46 | self.engine = MetaEngine(db) 47 | else: 48 | self.engine = Engine(db) 49 | 50 | def __getattr__(self, name: str) -> WrappedOperation: 51 | registered = self._registered_ops_.get(name) 52 | if registered is not None: 53 | return WrappedOperation(registered, name, self.engine) 54 | raise NotImplementedError 55 | 56 | 57 | class Column(sdict): 58 | def __init__(self, name: str, type: str = "string", unique: bool = False, notnull: bool = False, **kwargs: Any): 59 | self.name = name 60 | self.type = type 61 | self.unique = unique 62 | self.notnull = notnull 63 | for key, val in kwargs.items(): 64 | self[key] = val 65 | self.length: int = self.length or 255 66 | 67 | def _fk_type(self, db: Database, tablename: str): 68 | if self.name not in db[tablename]._model_._belongs_ref_: 69 | return 70 | ref = db[tablename]._model_._belongs_ref_[self.name] 71 | if ref.ftype != "id": 72 | self.type = ref.ftype 73 | self.length = db[ref.model][ref.fk].length 74 | self.on_delete = None 75 | 76 | @classmethod 77 | def from_field(cls, field: Field) -> Column: 78 | rv = cls( 79 | field.name, 80 | field._pydal_types.get(field._type, field._type), 81 | field.unique, 82 | field.notnull, 83 | length=field.length, 84 | ondelete=field.ondelete, 85 | **field._ormkw, 86 | ) 87 | if _feasible_as_dbms_default(field.default): 88 | rv.default = field.default 89 | rv._fk_type(field.db, field.tablename) 90 | return rv 91 | 92 | def __repr__(self) -> str: 93 | return "%s(%s)" % (self.__class__.__name__, ", ".join(["%s=%r" % (k, v) for k, v in self.items()])) 94 | -------------------------------------------------------------------------------- /emmett/orm/migrations/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.migrations.exceptions 4 | -------------------------------- 5 | 6 | Provides exceptions for migration operations. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | 13 | class RevisionError(Exception): 14 | pass 15 | 16 | 17 | class RangeNotAncestorError(RevisionError): 18 | def __init__(self, lower, upper): 19 | self.lower = lower 20 | self.upper = upper 21 | super(RangeNotAncestorError, self).__init__( 22 | "Revision %s is not an ancestor of revision %s" % (lower or "base", upper or "base") 23 | ) 24 | 25 | 26 | class MultipleHeads(RevisionError): 27 | def __init__(self, heads, argument): 28 | self.heads = heads 29 | self.argument = argument 30 | super(MultipleHeads, self).__init__( 31 | "Multiple heads are present for given argument '%s'; %s" % (argument, ", ".join(heads)) 32 | ) 33 | 34 | 35 | class ResolutionError(RevisionError): 36 | def __init__(self, message, argument): 37 | super(ResolutionError, self).__init__(message) 38 | self.argument = argument 39 | -------------------------------------------------------------------------------- /emmett/orm/migrations/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.migrations.helpers 4 | ----------------------------- 5 | 6 | Provides helpers for migrations. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from collections.abc import Iterable 15 | from contextlib import contextmanager 16 | from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, Type 17 | from uuid import uuid4 18 | 19 | from pydal.adapters.base import BaseAdapter 20 | 21 | from ...datastructures import _unique_list 22 | from .base import Database 23 | 24 | 25 | if TYPE_CHECKING: 26 | from .engine import MetaEngine 27 | from .operations import Operation 28 | 29 | 30 | DEFAULT_VALUE = lambda: None 31 | 32 | 33 | def make_migration_id(): 34 | return uuid4().hex[-12:] 35 | 36 | 37 | class WrappedOperation: 38 | def __init__(self, op_class: Type[Operation], name: str, engine: MetaEngine): 39 | self.op_class = op_class 40 | self.name = name 41 | self.engine = engine 42 | 43 | def __call__(self, *args: Any, **kwargs: Any) -> Any: 44 | op = getattr(self.op_class, self.name)(*args, **kwargs) 45 | op._env_load_(self.engine) 46 | return op.run() 47 | 48 | 49 | class Dispatcher: 50 | def __init__(self): 51 | self._registry: Dict[Type[Operation], Callable[[Operation], str]] = {} 52 | 53 | def dispatch_for( 54 | self, target: Type[Operation] 55 | ) -> Callable[[Callable[[Operation], str]], Callable[[Operation], str]]: 56 | def wrap(fn: Callable[[Operation], str]) -> Callable[[Operation], str]: 57 | self._registry[target] = fn 58 | return fn 59 | 60 | return wrap 61 | 62 | def dispatch(self, obj: Operation): 63 | targets = type(obj).__mro__ 64 | for target in targets: 65 | if target in self._registry: 66 | return self._registry[target] 67 | raise ValueError(f"no dispatch function for object: {obj}") 68 | 69 | 70 | class DryRunAdapter: 71 | def __init__(self, adapter: BaseAdapter, logger: Any): 72 | self.adapter = adapter 73 | self.__dlogger = logger 74 | 75 | def __getattr__(self, name: str) -> Any: 76 | return getattr(self.adapter, name) 77 | 78 | def execute(self, sql: str): 79 | self.__dlogger(sql) 80 | 81 | 82 | class DryRunDatabase: 83 | def __init__(self, db: Database, logger: Any): 84 | self.db = db 85 | self._adapter = DryRunAdapter(db._adapter, logger) 86 | 87 | def __getattr__(self, name: str) -> Any: 88 | return getattr(self.db, name) 89 | 90 | def __getitem__(self, key: str) -> Any: 91 | return self.db[key] 92 | 93 | @contextmanager 94 | def connection(self, *args: Any, **kwargs: Any) -> Generator[None, None, None]: 95 | yield None 96 | 97 | 98 | def to_tuple(x, default=None): 99 | if x is None: 100 | return default 101 | elif isinstance(x, str): 102 | return (x,) 103 | elif isinstance(x, Iterable): 104 | return tuple(x) 105 | else: 106 | return (x,) 107 | 108 | 109 | def tuple_or_value(val): 110 | if not val: 111 | return None 112 | elif len(val) == 1: 113 | return val[0] 114 | else: 115 | return val 116 | 117 | 118 | def tuple_rev_as_scalar(rev): 119 | if not rev: 120 | return None 121 | elif len(rev) == 1: 122 | return rev[0] 123 | else: 124 | return rev 125 | 126 | 127 | def dedupe_tuple(tup): 128 | return tuple(_unique_list(tup)) 129 | 130 | 131 | def format_with_comma(value): 132 | if value is None: 133 | return "" 134 | elif isinstance(value, str): 135 | return value 136 | elif isinstance(value, Iterable): 137 | return ", ".join(value) 138 | else: 139 | raise ValueError("Don't know how to comma-format %r" % value) 140 | 141 | 142 | def _feasible_as_dbms_default(val): 143 | if callable(val): 144 | return False 145 | if val is None: 146 | return True 147 | if isinstance(val, int): 148 | return True 149 | if isinstance(val, str): 150 | return True 151 | if isinstance(val, (bool, float)): 152 | return True 153 | return False 154 | -------------------------------------------------------------------------------- /emmett/orm/migrations/migration.tmpl: -------------------------------------------------------------------------------- 1 | """{{=message}} 2 | 3 | Migration ID: {{=up_migration}} 4 | Revises: {{=down_migration_str}} 5 | Creation Date: {{=creation_date}} 6 | 7 | """ 8 | 9 | from emmett.orm import migrations 10 | 11 | 12 | class Migration(migrations.Migration): 13 | revision = {{=asis("%r" % up_migration)}} 14 | revises = {{=asis(down_migration)}} 15 | 16 | def up(self): 17 | {{for upgrade in upgrades:}} 18 | {{=asis(upgrade)}} 19 | {{pass}} 20 | 21 | def down(self): 22 | {{for downgrade in downgrades:}} 23 | {{=asis(downgrade)}} 24 | {{pass}} 25 | -------------------------------------------------------------------------------- /emmett/orm/migrations/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.migrations.utilities 4 | ------------------------------- 5 | 6 | Provides some migration utilities. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from ..base import Database 13 | from .engine import Engine 14 | from .generation import Generator 15 | from .operations import MigrationOp, UpgradeOps 16 | 17 | 18 | class RuntimeGenerator(Generator): 19 | def _load_head_to_meta(self): 20 | pass 21 | 22 | 23 | class RuntimeMigration(MigrationOp): 24 | def __init__(self, engine: Engine, ops: UpgradeOps): 25 | super().__init__("runtime", ops, ops.reverse(), "runtime") 26 | self.engine = engine 27 | for op in self.upgrade_ops.ops: 28 | op.engine = self.engine 29 | for op in self.downgrade_ops.ops: 30 | op.engine = self.engine 31 | 32 | def up(self): 33 | for op in self.upgrade_ops.ops: 34 | op.run() 35 | 36 | def down(self): 37 | for op in self.downgrade_ops.ops: 38 | op.run() 39 | 40 | 41 | def generate_runtime_migration(db: Database) -> RuntimeMigration: 42 | engine = Engine(db) 43 | upgrade_ops = RuntimeGenerator.generate_from(db, None, None) 44 | return RuntimeMigration(engine, upgrade_ops) 45 | -------------------------------------------------------------------------------- /emmett/orm/transactions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.transactions 4 | ----------------------- 5 | 6 | Provides pyDAL advanced transactions implementation for Emmett. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | 10 | Parts of this code are inspired to peewee 11 | :copyright: (c) 2010 by Charles Leifer 12 | 13 | :license: BSD-3-Clause 14 | """ 15 | 16 | import uuid 17 | from functools import wraps 18 | 19 | 20 | class callable_context_manager(object): 21 | def __call__(self, fn): 22 | @wraps(fn) 23 | def inner(*args, **kwargs): 24 | with self: 25 | return fn(*args, **kwargs) 26 | 27 | return inner 28 | 29 | 30 | class _atomic(callable_context_manager): 31 | def __init__(self, adapter): 32 | self.adapter = adapter 33 | 34 | def __enter__(self): 35 | if self.adapter.transaction_depth() == 0: 36 | self._helper = self.adapter.db.transaction() 37 | else: 38 | self._helper = self.adapter.db.savepoint() 39 | return self._helper.__enter__() 40 | 41 | def __exit__(self, exc_type, exc_val, exc_tb): 42 | return self._helper.__exit__(exc_type, exc_val, exc_tb) 43 | 44 | 45 | class _transaction(callable_context_manager): 46 | def __init__(self, adapter, lock_type=None): 47 | self.adapter = adapter 48 | self._lock_type = lock_type 49 | self._ops = [] 50 | 51 | def _add_op(self, op): 52 | self._ops.append(op) 53 | 54 | def _add_ops(self, ops): 55 | self._ops.extend(ops) 56 | 57 | def _begin(self): 58 | if self._lock_type: 59 | self.adapter.begin(self._lock_type) 60 | else: 61 | self.adapter.begin() 62 | 63 | def commit(self, begin=True): 64 | for op in self._ops: 65 | for callback in op.table._before_commit: 66 | callback(op.op_type, op.context) 67 | for callback in getattr(op.table, f"_before_commit_{op.op_type}"): 68 | callback(op.context) 69 | self.adapter.commit() 70 | for op in self._ops: 71 | for callback in op.table._after_commit: 72 | callback(op.op_type, op.context) 73 | for callback in getattr(op.table, f"_after_commit_{op.op_type}"): 74 | callback(op.context) 75 | self._ops.clear() 76 | if begin: 77 | self._begin() 78 | 79 | def rollback(self, begin=True): 80 | self._ops.clear() 81 | self.adapter.rollback() 82 | if begin: 83 | self._begin() 84 | 85 | def __enter__(self): 86 | if self.adapter.transaction_depth() == 0: 87 | self._begin() 88 | self.adapter.push_transaction(self) 89 | return self 90 | 91 | def __exit__(self, exc_type, exc_val, exc_tb): 92 | try: 93 | if exc_type: 94 | self.rollback(False) 95 | elif self.adapter.transaction_depth() == 1: 96 | try: 97 | self.commit(False) 98 | except Exception: 99 | self.rollback(False) 100 | raise 101 | finally: 102 | self.adapter.pop_transaction() 103 | 104 | 105 | class _savepoint(callable_context_manager): 106 | def __init__(self, adapter, sid=None): 107 | self.adapter = adapter 108 | self.sid = sid or "s" + uuid.uuid4().hex 109 | self.quoted_sid = self.adapter.dialect.quote(self.sid) 110 | self._ops = [] 111 | self._parent = None 112 | 113 | def _add_op(self, op): 114 | self._ops.append(op) 115 | 116 | def _add_ops(self, ops): 117 | self._ops.extend(ops) 118 | 119 | def _begin(self): 120 | self.adapter.execute("SAVEPOINT %s;" % self.quoted_sid) 121 | 122 | def commit(self, begin=True): 123 | self.adapter.execute("RELEASE SAVEPOINT %s;" % self.quoted_sid) 124 | if begin: 125 | self._begin() 126 | 127 | def rollback(self): 128 | self._ops.clear() 129 | self.adapter.execute("ROLLBACK TO SAVEPOINT %s;" % self.quoted_sid) 130 | 131 | def __enter__(self): 132 | self._parent = self.adapter.top_transaction() 133 | self._begin() 134 | self.adapter.push_transaction(self) 135 | return self 136 | 137 | def __exit__(self, exc_type, exc_val, exc_tb): 138 | try: 139 | if exc_type: 140 | self.rollback() 141 | else: 142 | try: 143 | self.commit(begin=False) 144 | if self._parent: 145 | self._parent._add_ops(self._ops) 146 | except Exception: 147 | self.rollback() 148 | raise 149 | finally: 150 | self.adapter.pop_transaction() 151 | -------------------------------------------------------------------------------- /emmett/orm/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.orm.wrappers 4 | ------------------- 5 | 6 | Provides ORM wrappers utilities. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from .helpers import RelationBuilder 13 | from .objects import HasManySet, HasManyViaSet, HasOneSet, HasOneViaSet 14 | 15 | 16 | class Wrapper(object): 17 | def __init__(self, ref): 18 | self.__name__ = ref.name 19 | self.ref = ref 20 | 21 | 22 | class HasOneWrap(Wrapper): 23 | def __call__(self, model, row): 24 | return HasOneSet(model.db, RelationBuilder(self.ref, model), row) 25 | 26 | 27 | class HasOneViaWrap(Wrapper): 28 | def __call__(self, model, row): 29 | return HasOneViaSet(model.db, RelationBuilder(self.ref, model), row) 30 | 31 | 32 | class HasManyWrap(Wrapper): 33 | def __call__(self, model, row): 34 | return HasManySet(model.db, RelationBuilder(self.ref, model), row) 35 | 36 | 37 | class HasManyViaWrap(Wrapper): 38 | def __call__(self, model, row): 39 | return HasManyViaSet(model.db, RelationBuilder(self.ref, model), row) 40 | -------------------------------------------------------------------------------- /emmett/parsers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.parsers 4 | -------------- 5 | 6 | :copyright: 2014 Giovanni Barillari 7 | :license: BSD-3-Clause 8 | """ 9 | 10 | from emmett_core.parsers import Parsers as Parsers 11 | -------------------------------------------------------------------------------- /emmett/pipeline.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.pipeline 4 | --------------- 5 | 6 | Provides the pipeline classes. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import types 13 | 14 | from emmett_core.http.helpers import redirect 15 | from emmett_core.pipeline.extras import RequirePipe as _RequirePipe 16 | from emmett_core.pipeline.pipe import Pipe as Pipe 17 | 18 | from .ctx import current 19 | from .helpers import flash 20 | 21 | 22 | class RequirePipe(_RequirePipe): 23 | __slots__ = ["flash"] 24 | _current = current 25 | 26 | def __init__(self, condition=None, otherwise=None, flash=True): 27 | super().__init__(condition=condition, otherwise=otherwise) 28 | self.flash = flash 29 | 30 | async def pipe_request(self, next_pipe, **kwargs): 31 | flag = self.condition() 32 | if not flag: 33 | if self.otherwise is not None: 34 | if callable(self.otherwise): 35 | return self.otherwise() 36 | redirect(self.__class__._current, self.otherwise) 37 | else: 38 | if self.flash: 39 | flash("Insufficient privileges") 40 | redirect(self.__class__._current, "/") 41 | return await next_pipe(**kwargs) 42 | 43 | 44 | class Injector(Pipe): 45 | namespace: str = "__global__" 46 | 47 | def __init__(self): 48 | self._injections_ = {} 49 | if self.namespace != "__global__": 50 | self._inject = self._inject_local 51 | return 52 | self._inject = self._inject_global 53 | for attr_name in set(dir(self)) - self.__class__._pipeline_methods_ - {"output", "namespace"}: 54 | if attr_name.startswith("_"): 55 | continue 56 | attr = getattr(self, attr_name) 57 | if isinstance(attr, types.MethodType): 58 | self._injections_[attr_name] = self._wrapped_method(attr) 59 | continue 60 | self._injections_[attr_name] = attr 61 | 62 | @staticmethod 63 | def _wrapped_method(method): 64 | def wrap(*args, **kwargs): 65 | return method(*args, **kwargs) 66 | 67 | return wrap 68 | 69 | def _inject_local(self, ctx): 70 | ctx[self.namespace] = self 71 | 72 | def _inject_global(self, ctx): 73 | ctx.update(self._injections_) 74 | 75 | async def pipe_request(self, next_pipe, **kwargs): 76 | ctx = await next_pipe(**kwargs) 77 | if isinstance(ctx, dict): 78 | self._inject(ctx) 79 | return ctx 80 | -------------------------------------------------------------------------------- /emmett/routing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/routing/__init__.py -------------------------------------------------------------------------------- /emmett/routing/response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.routing.response 4 | ----------------------- 5 | 6 | Provides response builders for http routes. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from typing import Any, Dict, Tuple, Union 15 | 16 | from emmett_core.http.response import HTTPStringResponse 17 | from emmett_core.routing.response import ResponseProcessor 18 | from renoir.errors import TemplateMissingError 19 | 20 | from ..ctx import current 21 | from ..helpers import load_component 22 | from ..html import asis 23 | from .urls import url 24 | 25 | 26 | _html_content_type = "text/html; charset=utf-8" 27 | 28 | 29 | class TemplateResponseBuilder(ResponseProcessor): 30 | def process(self, output: Union[Dict[str, Any], None], response) -> str: 31 | response.headers._data["content-type"] = _html_content_type 32 | base_ctx = {"current": current, "url": url, "asis": asis, "load_component": load_component} 33 | output = base_ctx if output is None else {**base_ctx, **output} 34 | try: 35 | return self.route.app.templater.render(self.route.template, output) 36 | except TemplateMissingError as exc: 37 | raise HTTPStringResponse(404, body="{}\n".format(exc.message), cookies=response.cookies) 38 | 39 | 40 | class SnippetResponseBuilder(ResponseProcessor): 41 | def process(self, output: Tuple[str, Union[Dict[str, Any], None]], response) -> str: 42 | response.headers._data["content-type"] = _html_content_type 43 | template, output = output 44 | base_ctx = {"current": current, "url": url, "asis": asis, "load_component": load_component} 45 | output = base_ctx if output is None else {**base_ctx, **output} 46 | return self.route.app.templater._render(template, f"_snippet.{current.request.name}", output) 47 | 48 | 49 | class AutoResponseBuilder(ResponseProcessor): 50 | def process(self, output: Any, response) -> str: 51 | is_template, snippet = False, None 52 | if isinstance(output, tuple): 53 | snippet, output = output 54 | if isinstance(output, dict): 55 | is_template = True 56 | output = {**{"current": current, "url": url, "asis": asis, "load_component": load_component}, **output} 57 | elif output is None: 58 | is_template = True 59 | output = {"current": current, "url": url, "asis": asis, "load_component": load_component} 60 | if is_template: 61 | response.headers._data["content-type"] = _html_content_type 62 | if snippet is not None: 63 | return self.route.app.templater._render(snippet, f"_snippet.{current.request.name}", output) 64 | try: 65 | return self.route.app.templater.render(self.route.template, output) 66 | except TemplateMissingError as exc: 67 | raise HTTPStringResponse(404, body="{}\n".format(exc.message), cookies=response.cookies) 68 | if isinstance(output, str): 69 | return output 70 | return str(output) 71 | -------------------------------------------------------------------------------- /emmett/routing/router.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.routing.router 4 | --------------------- 5 | 6 | Provides router implementations. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from emmett_core.routing.router import ( 15 | HTTPRouter as _HTTPRouter, 16 | RoutingCtx as RoutingCtx, 17 | RoutingCtxGroup as RoutingCtxGroup, 18 | WebsocketRouter as WebsocketRouter, 19 | ) 20 | 21 | from .response import AutoResponseBuilder, SnippetResponseBuilder, TemplateResponseBuilder 22 | from .rules import HTTPRoutingRule 23 | 24 | 25 | class HTTPRouter(_HTTPRouter): 26 | __slots__ = ["injectors"] 27 | 28 | _routing_rule_cls = HTTPRoutingRule 29 | _outputs = { 30 | **_HTTPRouter._outputs, 31 | **{ 32 | "auto": AutoResponseBuilder, 33 | "template": TemplateResponseBuilder, 34 | "snippet": SnippetResponseBuilder, 35 | }, 36 | } 37 | 38 | def __init__(self, *args, **kwargs): 39 | super().__init__(*args, **kwargs) 40 | self.injectors = [] 41 | -------------------------------------------------------------------------------- /emmett/routing/routes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.routing.routes 4 | --------------------- 5 | 6 | Provides routes objects. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import re 13 | from functools import wraps 14 | 15 | import pendulum 16 | from emmett_core.http.response import HTTPResponse 17 | from emmett_core.routing.routes import HTTPRoute as _HTTPRoute 18 | 19 | 20 | class HTTPRoute(_HTTPRoute): 21 | __slots__ = [] 22 | 23 | def __init__(self, rule, path, idx): 24 | super().__init__(rule, path, idx) 25 | self.build_argparser() 26 | 27 | def build_argparser(self): 28 | parsers = {"date": self._parse_date_reqarg} 29 | opt_parsers = {"date": self._parse_date_reqarg_opt} 30 | pipeline = [] 31 | for key in parsers.keys(): 32 | optionals = [] 33 | for element in re.compile(r"\(([^<]+)?<{}\:(\w+)>\)\?".format(key)).findall(self.path): 34 | optionals.append(element[1]) 35 | elements = set(re.compile(r"<{}\:(\w+)>".format(key)).findall(self.path)) 36 | args = elements - set(optionals) 37 | if args: 38 | parser = self._wrap_reqargs_parser(parsers[key], args) 39 | pipeline.append(parser) 40 | if optionals: 41 | parser = self._wrap_reqargs_parser(opt_parsers[key], optionals) 42 | pipeline.append(parser) 43 | if pipeline: 44 | for key, dispatcher in self.dispatchers.items(): 45 | self.dispatchers[key] = DispacherWrapper(dispatcher, pipeline) 46 | 47 | @staticmethod 48 | def _parse_date_reqarg(args, route_args): 49 | try: 50 | for arg in args: 51 | dt = route_args[arg] 52 | route_args[arg] = pendulum.datetime(dt.year, dt.month, dt.day) 53 | except Exception: 54 | raise HTTPResponse(404) 55 | 56 | @staticmethod 57 | def _parse_date_reqarg_opt(args, route_args): 58 | try: 59 | for arg in args: 60 | if route_args[arg] is None: 61 | continue 62 | dt = route_args[arg] 63 | route_args[arg] = pendulum.datetime(dt.year, dt.month, dt.day) 64 | except Exception: 65 | raise HTTPResponse(404) 66 | 67 | @staticmethod 68 | def _wrap_reqargs_parser(parser, args): 69 | @wraps(parser) 70 | def wrapped(route_args): 71 | return parser(args, route_args) 72 | 73 | return wrapped 74 | 75 | 76 | class DispacherWrapper: 77 | __slots__ = ["dispatcher", "parsers"] 78 | 79 | def __init__(self, dispatcher, parsers): 80 | self.dispatcher = dispatcher 81 | self.parsers = parsers 82 | 83 | def dispatch(self, reqargs, response): 84 | for parser in self.parsers: 85 | parser(reqargs) 86 | return self.dispatcher.dispatch(reqargs, response) 87 | -------------------------------------------------------------------------------- /emmett/routing/rules.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.routing.rules 4 | -------------------- 5 | 6 | Provides routing rules definition apis. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import os 15 | from typing import Any, Callable 16 | 17 | from emmett_core.routing.rules import HTTPRoutingRule as _HTTPRoutingRule 18 | 19 | from ..ctx import current 20 | from .routes import HTTPRoute 21 | 22 | 23 | class HTTPRoutingRule(_HTTPRoutingRule): 24 | __slots__ = ["injectors", "template_folder", "template_path", "template"] 25 | current = current 26 | route_cls = HTTPRoute 27 | 28 | def __init__( 29 | self, 30 | router, 31 | paths=None, 32 | name=None, 33 | template=None, 34 | pipeline=None, 35 | injectors=None, 36 | schemes=None, 37 | hostname=None, 38 | methods=None, 39 | prefix=None, 40 | template_folder=None, 41 | template_path=None, 42 | cache=None, 43 | output="auto", 44 | ): 45 | super().__init__( 46 | router, 47 | paths=paths, 48 | name=name, 49 | pipeline=pipeline, 50 | schemes=schemes, 51 | hostname=hostname, 52 | methods=methods, 53 | prefix=prefix, 54 | cache=cache, 55 | output=output, 56 | ) 57 | self.template = template 58 | self.template_folder = template_folder 59 | self.template_path = template_path or self.app.template_path 60 | self.pipeline = self.pipeline + self.router.injectors + (injectors or []) 61 | 62 | def _make_builders(self, output_type): 63 | builder_cls = self.router._outputs[output_type] 64 | return builder_cls(self), self.router._outputs["empty"](self) 65 | 66 | def __call__(self, f: Callable[..., Any]) -> Callable[..., Any]: 67 | if not self.template: 68 | self.template = f.__name__ + self.app.template_default_extension 69 | if self.template_folder: 70 | self.template = os.path.join(self.template_folder, self.template) 71 | return super().__call__(f) 72 | -------------------------------------------------------------------------------- /emmett/routing/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.routing.urls 4 | ------------------- 5 | 6 | Provides url builder apis. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.routing.urls import Url 13 | 14 | from ..ctx import current 15 | 16 | 17 | url = Url(current) 18 | -------------------------------------------------------------------------------- /emmett/rsgi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/rsgi/__init__.py -------------------------------------------------------------------------------- /emmett/rsgi/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.rsgi.handlers 4 | -------------------- 5 | 6 | Provides RSGI handlers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import asyncio 15 | import os 16 | from typing import Awaitable, Callable 17 | 18 | from emmett_core.http.response import HTTPResponse, HTTPStringResponse 19 | from emmett_core.protocols.rsgi.handlers import HTTPHandler as _HTTPHandler, WSHandler as _WSHandler, WSTransport 20 | from emmett_core.protocols.rsgi.helpers import noop_response 21 | from emmett_core.utils import cachedprop 22 | 23 | from ..ctx import RequestContext, WSContext, current 24 | from ..debug import debug_handler, smart_traceback 25 | from .wrappers import Request, Response, Websocket 26 | 27 | 28 | class HTTPHandler(_HTTPHandler): 29 | __slots__ = [] 30 | wapper_cls = Request 31 | response_cls = Response 32 | 33 | @cachedprop 34 | def error_handler(self) -> Callable[[], Awaitable[str]]: 35 | return self._debug_handler if self.app.debug else self.exception_handler 36 | 37 | def _static_handler(self, scope, protocol, path: str) -> Awaitable[HTTPResponse]: 38 | #: handle internal assets 39 | if path.startswith("/__emmett__"): 40 | file_name = path[12:] 41 | if not file_name: 42 | return self._http_response(404) 43 | static_file = os.path.join(os.path.dirname(__file__), "..", "assets", file_name) 44 | if os.path.splitext(static_file)[1] == "html": 45 | return self._http_response(404) 46 | return self._static_response(static_file) 47 | #: handle app assets 48 | static_file, _ = self.static_matcher(path) 49 | if static_file: 50 | return self._static_response(static_file) 51 | return self.dynamic_handler(scope, protocol, path) 52 | 53 | async def _debug_handler(self) -> str: 54 | current.response.headers._data["content-type"] = "text/html; charset=utf-8" 55 | return debug_handler(smart_traceback(self.app)) 56 | 57 | async def dynamic_handler(self, scope, protocol, path: str) -> HTTPResponse: 58 | request = Request( 59 | scope, 60 | path, 61 | protocol, 62 | max_content_length=self.app.config.request_max_content_length, 63 | max_multipart_size=self.app.config.request_multipart_max_size, 64 | body_timeout=self.app.config.request_body_timeout, 65 | ) 66 | response = Response(protocol) 67 | ctx = RequestContext(self.app, request, response) 68 | ctx_token = current._init_(ctx) 69 | try: 70 | http = await self.router.dispatch(request, response) 71 | except HTTPResponse as http_exception: 72 | http = http_exception 73 | #: render error with handlers if in app 74 | error_handler = self.app.error_handlers.get(http.status_code) 75 | if error_handler: 76 | http = HTTPStringResponse( 77 | http.status_code, await error_handler(), headers=response.headers, cookies=response.cookies 78 | ) 79 | except asyncio.CancelledError: 80 | http = noop_response 81 | except Exception: 82 | self.app.log.exception("Application exception:") 83 | http = HTTPStringResponse(500, await self.error_handler(), headers=response.headers) 84 | finally: 85 | current._close_(ctx_token) 86 | return http 87 | 88 | 89 | class WSHandler(_WSHandler): 90 | wrapper_cls = Websocket 91 | 92 | async def dynamic_handler(self, scope, transport: WSTransport, path: str): 93 | ctx = WSContext(self.app, Websocket(scope, path, transport)) 94 | ctx_token = current._init_(ctx) 95 | try: 96 | await self.router.dispatch(ctx.websocket) 97 | except HTTPResponse as http: 98 | transport.status = http.status_code 99 | except asyncio.CancelledError: 100 | if not transport.interrupted: 101 | self.app.log.exception("Application exception:") 102 | except Exception: 103 | transport.status = 500 104 | self.app.log.exception("Application exception:") 105 | finally: 106 | current._close_(ctx_token) 107 | -------------------------------------------------------------------------------- /emmett/rsgi/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.rsgi.wrappers 4 | -------------------- 5 | 6 | Provides RSGI request and websocket wrappers 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import pendulum 13 | from emmett_core.protocols.rsgi.wrappers import Request as _Request, Response as _Response, Websocket as Websocket 14 | from emmett_core.utils import cachedprop 15 | 16 | from ..wrappers.response import ResponseMixin 17 | 18 | 19 | class Request(_Request): 20 | __slots__ = [] 21 | 22 | @cachedprop 23 | def now(self) -> pendulum.DateTime: 24 | return pendulum.instance(self._now) 25 | 26 | @cachedprop 27 | def now_local(self) -> pendulum.DateTime: 28 | return self.now.in_timezone(pendulum.local_timezone()) # type: ignore 29 | 30 | 31 | class Response(ResponseMixin, _Response): 32 | __slots__ = [] 33 | -------------------------------------------------------------------------------- /emmett/security.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.security 4 | --------------- 5 | 6 | Miscellaneous security helpers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import hashlib 13 | import hmac 14 | import time 15 | from collections import OrderedDict 16 | from uuid import uuid4 17 | 18 | from emmett_core.cryptography import kdf 19 | 20 | # TODO: check bytes conversions 21 | from ._shortcuts import to_bytes 22 | 23 | 24 | class CSRFStorage(OrderedDict): 25 | def _clean(self): 26 | now = time.time() 27 | for key in list(self): 28 | if self[key] + 3600 > now: 29 | break 30 | del self[key] 31 | 32 | def gen_token(self): 33 | self._clean() 34 | token = str(uuid4()) 35 | self[token] = int(time.time()) 36 | return token 37 | 38 | 39 | def md5_hash(text): 40 | """Generate a md5 hash with the given text""" 41 | return hashlib.md5(text).hexdigest() 42 | 43 | 44 | def simple_hash(text, key="", salt="", digest_alg="md5"): 45 | """ 46 | Generates hash with the given text using the specified 47 | digest hashing algorithm 48 | """ 49 | if not digest_alg: 50 | raise RuntimeError("simple_hash with digest_alg=None") 51 | elif not isinstance(digest_alg, str): # manual approach 52 | h = digest_alg(text + key + salt) 53 | elif digest_alg.startswith("pbkdf2"): # latest and coolest! 54 | iterations, keylen, alg = digest_alg[7:-1].split(",") 55 | return kdf.pbkdf2_hex( 56 | text, salt, iterations=int(iterations), keylen=int(keylen), hash_algorithm=kdf.PBKDF2_HMAC[alg] 57 | ) 58 | elif key: # use hmac 59 | digest_alg = get_digest(digest_alg) 60 | h = hmac.new(to_bytes(key + salt), msg=to_bytes(text), digestmod=digest_alg) 61 | else: # compatible with third party systems 62 | h = hashlib.new(digest_alg) 63 | h.update(to_bytes(text + salt)) 64 | return h.hexdigest() 65 | 66 | 67 | def get_digest(value): 68 | """ 69 | Returns a hashlib digest algorithm from a string 70 | """ 71 | if not isinstance(value, str): 72 | return value 73 | value = value.lower() 74 | if value == "md5": 75 | return hashlib.md5 76 | elif value == "sha1": 77 | return hashlib.sha1 78 | elif value == "sha224": 79 | return hashlib.sha224 80 | elif value == "sha256": 81 | return hashlib.sha256 82 | elif value == "sha384": 83 | return hashlib.sha384 84 | elif value == "sha512": 85 | return hashlib.sha512 86 | else: 87 | raise ValueError("Invalid digest algorithm: %s" % value) 88 | 89 | 90 | DIGEST_ALG_BY_SIZE = { 91 | 128 / 4: "md5", 92 | 160 / 4: "sha1", 93 | 224 / 4: "sha224", 94 | 256 / 4: "sha256", 95 | 384 / 4: "sha384", 96 | 512 / 4: "sha512", 97 | } 98 | -------------------------------------------------------------------------------- /emmett/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.serializers 4 | ------------------ 5 | 6 | :copyright: 2014 Giovanni Barillari 7 | :license: BSD-3-Clause 8 | """ 9 | 10 | from emmett_core.serializers import Serializers as Serializers 11 | 12 | from .html import htmlescape, tag 13 | 14 | 15 | def xml_encode(value, key=None, quote=True): 16 | if hasattr(value, "__xml__"): 17 | return value.__xml__(key, quote) 18 | if isinstance(value, dict): 19 | return tag[key](*[tag[k](xml_encode(v, None, quote)) for k, v in value.items()]) 20 | if isinstance(value, list): 21 | return tag[key](*[tag[item](xml_encode(item, None, quote)) for item in value]) 22 | return htmlescape(value) 23 | 24 | 25 | @Serializers.register_for("xml") 26 | def xml(value, encoding="UTF-8", key="document", quote=True): 27 | rv = ('' % encoding) + str(xml_encode(value, key, quote)) 28 | return rv 29 | -------------------------------------------------------------------------------- /emmett/sessions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.sessions 4 | --------------- 5 | 6 | Provides session managers for applications. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | from emmett_core.sessions import SessionManager as _SessionManager 15 | 16 | from .ctx import current 17 | 18 | 19 | class SessionManager(_SessionManager): 20 | @classmethod 21 | def _build_pipe(cls, handler_cls, *args, **kwargs): 22 | cls._pipe = handler_cls(current, *args, **kwargs) 23 | return cls._pipe 24 | -------------------------------------------------------------------------------- /emmett/templating/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/templating/__init__.py -------------------------------------------------------------------------------- /emmett/templating/lexers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.templating.lexers 4 | ------------------------ 5 | 6 | Provides the Emmett lexers for Renoir engine. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from renoir import Lexer 13 | 14 | from ..ctx import current 15 | from ..routing.urls import url 16 | 17 | 18 | class HelpersLexer(Lexer): 19 | helpers = [ 20 | '', 21 | '', 22 | ] 23 | 24 | def process(self, ctx, value): 25 | for helper in self.helpers: 26 | ctx.html(helper.format(current.app._router_http._prefix_main)) 27 | 28 | 29 | class MetaLexer(Lexer): 30 | def process(self, ctx, value): 31 | ctx.python_node("for name, value in current.response._meta_tmpl():") 32 | ctx.variable('\'\' % (name, value)', escape=False) 33 | ctx.python_node("pass") 34 | ctx.python_node("for name, value in current.response._meta_tmpl_prop():") 35 | ctx.variable('\'\' % (name, value)', escape=False) 36 | ctx.python_node("pass") 37 | 38 | 39 | class StaticLexer(Lexer): 40 | evaluate = True 41 | 42 | def process(self, ctx, value): 43 | file_name = value.split("?")[0] 44 | surl = url("static", file_name) 45 | file_ext = file_name.rsplit(".", 1)[-1] 46 | if file_ext == "js": 47 | s = '' % surl 48 | elif file_ext == "css": 49 | s = '' % surl 50 | else: 51 | s = None 52 | if s: 53 | ctx.html(s) 54 | 55 | 56 | lexers = {"include_helpers": HelpersLexer(), "include_meta": MetaLexer(), "include_static": StaticLexer()} 57 | -------------------------------------------------------------------------------- /emmett/templating/templater.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.templating.templater 4 | --------------------------- 5 | 6 | Provides the Emmett implementation for Renoir engine. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import os 13 | from functools import reduce 14 | from typing import Optional, Tuple 15 | 16 | from renoir import Renoir 17 | 18 | from .lexers import lexers 19 | 20 | 21 | class Templater(Renoir): 22 | def __init__(self, **kwargs): 23 | kwargs["lexers"] = lexers 24 | super().__init__(**kwargs) 25 | self._namespaces = {} 26 | 27 | def _set_reload(self, value): 28 | self.cache.changes = value 29 | self.cache.load._configure() 30 | self.cache.prerender._configure() 31 | self.cache.parse._configure() 32 | 33 | def _set_encoding(self, value): 34 | self.encoding = value 35 | 36 | def _set_escape(self, value): 37 | self.escape = value 38 | self._configure() 39 | 40 | def _set_indent(self, value): 41 | self.indent = value 42 | self._configure() 43 | 44 | def register_namespace(self, namespace: str, path: Optional[str] = None): 45 | path = path or self.path 46 | self._namespaces[namespace] = path 47 | 48 | def _get_namespace_path_elements(self, file_name: str, path: Optional[str]) -> Tuple[str, str]: 49 | if ":" in file_name: 50 | namespace, file_name = file_name.split(":") 51 | path = self._namespaces.get(namespace, self.path) 52 | else: 53 | path = path or self.path 54 | return path, file_name 55 | 56 | def _preload(self, file_name: str, path: Optional[str] = None): 57 | path, file_name = self._get_namespace_path_elements(file_name, path) 58 | file_extension = os.path.splitext(file_name)[1] 59 | return reduce( 60 | lambda args, loader: loader(args[0], args[1]), self.loaders.get(file_extension, []), (path, file_name) 61 | ) 62 | 63 | def _no_preload(self, file_name: str, path: Optional[str] = None): 64 | return self._get_namespace_path_elements(file_name, path) 65 | -------------------------------------------------------------------------------- /emmett/testing.py: -------------------------------------------------------------------------------- 1 | from emmett_core.protocols.rsgi.test_client.client import ( 2 | ClientContext as _ClientContext, 3 | ClientHTTPHandlerMixin, 4 | EmmettTestClient as _EmmettTestClient, 5 | ) 6 | 7 | from .ctx import current 8 | from .rsgi.handlers import HTTPHandler 9 | from .rsgi.wrappers import Response 10 | 11 | 12 | class ClientContextResponse(Response): 13 | def __init__(self, original_response: Response): 14 | super().__init__(original_response._proto) 15 | self.status = original_response.status 16 | self.headers._data.update(original_response.headers._data) 17 | self.cookies.update(original_response.cookies.copy()) 18 | self.__dict__.update(original_response.__dict__) 19 | 20 | 21 | class ClientContext(_ClientContext): 22 | _response_wrap_cls = ClientContextResponse 23 | 24 | def __init__(self, ctx): 25 | super().__init__(ctx) 26 | self.T = current.T 27 | 28 | 29 | class ClientHTTPHandler(ClientHTTPHandlerMixin, HTTPHandler): 30 | _client_ctx_cls = ClientContext 31 | 32 | 33 | class EmmettTestClient(_EmmettTestClient): 34 | _current = current 35 | _handler_cls = ClientHTTPHandler 36 | -------------------------------------------------------------------------------- /emmett/tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import Auth 2 | from .decorators import requires, service, sse, stream 3 | from .mailer import Mailer 4 | from .service import ServicePipe 5 | from .stream import StreamPipe 6 | -------------------------------------------------------------------------------- /emmett/tools/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .apis import Auth 2 | from .models import AuthUser 3 | -------------------------------------------------------------------------------- /emmett/tools/auth/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.tools.auth.forms 4 | ----------------------- 5 | 6 | Provides the forms for the authorization system. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from ...forms import Form, ModelForm 13 | from ...orm import Field 14 | 15 | 16 | class AuthForms(object): 17 | _registry_ = {} 18 | _fields_registry_ = {} 19 | 20 | @classmethod 21 | def register_for(cls, target, fields=lambda: None): 22 | def wrap(f): 23 | cls._registry_[target] = f 24 | cls._fields_registry_[target] = fields 25 | return f 26 | 27 | return wrap 28 | 29 | @classmethod 30 | def get_for(cls, target): 31 | return cls._registry_[target], cls._fields_registry_[target] 32 | 33 | @classmethod 34 | def map(cls): 35 | rv = {} 36 | for key in cls._registry_.keys(): 37 | rv[key] = cls.get_for(key) 38 | return rv 39 | 40 | 41 | def login_fields(auth): 42 | model = auth.models["user"] 43 | rv = { 44 | "email": Field(validation={"is": "email", "presence": True}, label=model.email.label), 45 | "password": Field("password", validation=model.password._requires, label=model.password.label), 46 | } 47 | if auth.ext.config.remember_option: 48 | rv["remember"] = Field("bool", default=True, label=auth.ext.config.messages["remember_button"]) 49 | return rv 50 | 51 | 52 | def registration_fields(auth): 53 | rw_data = auth.models["user"]._instance_()._merged_form_rw_ 54 | user_table = auth.models["user"].table 55 | all_fields = [(field_name, user_table[field_name].clone()) for field_name in rw_data["registration"]["writable"]] 56 | for i, (field_name, _) in enumerate(all_fields): 57 | if field_name == "password": 58 | all_fields.insert( 59 | i + 1, ("password2", Field("password", label=auth.ext.config.messages["verify_password"])) 60 | ) 61 | break 62 | rv = {} 63 | for i, (field_name, field) in enumerate(all_fields): 64 | field.writable = True 65 | rv[field_name] = field 66 | rv[field_name]._inst_count_ = i 67 | return rv 68 | 69 | 70 | def profile_fields(auth): 71 | rw_data = auth.models["user"]._instance_()._merged_form_rw_ 72 | return rw_data["profile"] 73 | 74 | 75 | def password_retrieval_fields(auth): 76 | rv = { 77 | "email": Field(validation={"is": "email", "presence": True, "lower": True}), 78 | } 79 | return rv 80 | 81 | 82 | def password_reset_fields(auth): 83 | password_field = auth.ext.config.models["user"].password 84 | rv = { 85 | "password": Field( 86 | "password", validation=password_field._requires, label=auth.ext.config.messages["new_password"] 87 | ), 88 | "password2": Field("password", label=auth.ext.config.messages["verify_password"]), 89 | } 90 | return rv 91 | 92 | 93 | def password_change_fields(auth): 94 | password_validation = auth.ext.config.models["user"].password._requires 95 | rv = { 96 | "old_password": Field( 97 | "password", validation=password_validation, label=auth.ext.config.messages["old_password"] 98 | ), 99 | "new_password": Field( 100 | "password", validation=password_validation, label=auth.ext.config.messages["new_password"] 101 | ), 102 | "new_password2": Field("password", label=auth.ext.config.messages["verify_password"]), 103 | } 104 | return rv 105 | 106 | 107 | @AuthForms.register_for("login", fields=login_fields) 108 | def login_form(auth, fields, **kwargs): 109 | opts = {"submit": auth.ext.config.messages["login_button"], "keepvalues": True} 110 | opts.update(**kwargs) 111 | return Form(fields, **opts) 112 | 113 | 114 | @AuthForms.register_for("registration", fields=registration_fields) 115 | def registration_form(auth, fields, **kwargs): 116 | opts = {"submit": auth.ext.config.messages["registration_button"], "keepvalues": True} 117 | opts.update(**kwargs) 118 | return Form(fields, **opts) 119 | 120 | 121 | @AuthForms.register_for("profile", fields=profile_fields) 122 | def profile_form(auth, fields, **kwargs): 123 | opts = {"submit": auth.ext.config.messages["profile_button"], "keepvalues": True} 124 | opts.update(**kwargs) 125 | return ModelForm( 126 | auth.models["user"], record_id=auth.user.id, fields=fields, upload=auth.ext.exposer.url("download"), **opts 127 | ) 128 | 129 | 130 | @AuthForms.register_for("password_retrieval", fields=password_retrieval_fields) 131 | def password_retrieval_form(auth, fields, **kwargs): 132 | opts = {"submit": auth.ext.config.messages["password_retrieval_button"]} 133 | opts.update(**kwargs) 134 | return Form(fields, **opts) 135 | 136 | 137 | @AuthForms.register_for("password_reset", fields=password_reset_fields) 138 | def password_reset_form(auth, fields, **kwargs): 139 | opts = {"submit": auth.ext.config.messages["password_reset_button"], "keepvalues": True} 140 | opts.update(**kwargs) 141 | return Form(fields, **opts) 142 | 143 | 144 | @AuthForms.register_for("password_change", fields=password_change_fields) 145 | def password_change_form(auth, fields, **kwargs): 146 | opts = {"submit": auth.ext.config.messages["password_change_button"], "keepvalues": True} 147 | opts.update(**kwargs) 148 | return Form(fields, **opts) 149 | -------------------------------------------------------------------------------- /emmett/tools/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.tools.decorators 4 | ----------------------- 5 | 6 | Provides requires and service decorators. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.pipeline.dyn import ( 13 | ServicePipeBuilder as _ServicePipeBuilder, 14 | requires as _requires, 15 | service as _service, 16 | sse as _sse, 17 | stream as _stream, 18 | ) 19 | 20 | from ..pipeline import RequirePipe 21 | from .service import JSONServicePipe, XMLServicePipe 22 | from .stream import SSEPipe, StreamPipe 23 | 24 | 25 | class ServicePipeBuilder(_ServicePipeBuilder): 26 | _pipe_cls = {"json": JSONServicePipe, "xml": XMLServicePipe} 27 | 28 | 29 | class requires(_requires): 30 | _pipe_cls = RequirePipe 31 | 32 | 33 | class stream(_stream): 34 | _pipe_cls = StreamPipe 35 | 36 | 37 | class sse(_sse): 38 | _pipe_cls = SSEPipe 39 | 40 | 41 | class service(_service): 42 | _inner_builder = ServicePipeBuilder() 43 | 44 | @staticmethod 45 | def xml(f): 46 | return service("xml")(f) 47 | -------------------------------------------------------------------------------- /emmett/tools/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.tools.service 4 | -------------------- 5 | 6 | Provides the services handler. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.pipeline.extras import JSONPipe 13 | 14 | from ..ctx import current 15 | from ..pipeline import Pipe 16 | from ..serializers import Serializers 17 | 18 | 19 | class JSONServicePipe(JSONPipe): 20 | __slots__ = [] 21 | _current = current 22 | 23 | 24 | class XMLServicePipe(Pipe): 25 | __slots__ = ["encoder"] 26 | output = "str" 27 | 28 | def __init__(self): 29 | self.encoder = Serializers.get_for("xml") 30 | 31 | async def pipe_request(self, next_pipe, **kwargs): 32 | current.response.headers._data["content-type"] = "text/xml" 33 | return self.encoder(await next_pipe(**kwargs)) 34 | 35 | def on_send(self, data): 36 | return self.encoder(data) 37 | 38 | 39 | def ServicePipe(procedure: str) -> Pipe: 40 | pipe_cls = {"json": JSONServicePipe, "xml": XMLServicePipe}.get(procedure) 41 | if not pipe_cls: 42 | raise RuntimeError("Emmett cannot handle the service you requested: %s" % procedure) 43 | return pipe_cls() 44 | -------------------------------------------------------------------------------- /emmett/tools/stream.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.tools.stream 4 | ------------------- 5 | 6 | Provides the stream handlers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.pipeline.extras import SSEPipe as _SSEPipe, StreamPipe as _StreamPipe 13 | 14 | from ..ctx import current 15 | 16 | 17 | class StreamPipe(_StreamPipe): 18 | _current = current 19 | 20 | 21 | class SSEPipe(_SSEPipe): 22 | _current = current 23 | -------------------------------------------------------------------------------- /emmett/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.utils 4 | ------------ 5 | 6 | Provides some utilities for Emmett. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from __future__ import annotations 13 | 14 | import re 15 | import socket 16 | from datetime import date, datetime, time 17 | 18 | import pendulum 19 | from emmett_core.utils import cachedprop as cachedprop 20 | from pendulum.parsing import _parse as _pendulum_parse 21 | 22 | from .datastructures import sdict 23 | 24 | 25 | _pendulum_parsing_opts = {"day_first": False, "year_first": True, "strict": True, "exact": False, "now": None} 26 | 27 | 28 | def _pendulum_normalize(obj): 29 | if isinstance(obj, time): 30 | now = datetime.utcnow() 31 | obj = datetime(now.year, now.month, now.day, obj.hour, obj.minute, obj.second, obj.microsecond) 32 | elif isinstance(obj, date) and not isinstance(obj, datetime): 33 | obj = datetime(obj.year, obj.month, obj.day) 34 | return obj 35 | 36 | 37 | def parse_datetime(text): 38 | parsed = _pendulum_normalize(_pendulum_parse(text, **_pendulum_parsing_opts)) 39 | return pendulum.datetime( 40 | parsed.year, 41 | parsed.month, 42 | parsed.day, 43 | parsed.hour, 44 | parsed.minute, 45 | parsed.second, 46 | parsed.microsecond, 47 | tz=parsed.tzinfo or pendulum.UTC, 48 | ) 49 | 50 | 51 | _re_ipv4 = re.compile(r"(\d+)\.(\d+)\.(\d+)\.(\d+)") 52 | 53 | 54 | def is_valid_ip_address(address): 55 | # deal with special cases 56 | if address.lower() in ["127.0.0.1", "localhost", "::1", "::ffff:127.0.0.1"]: 57 | return True 58 | elif address.lower() in ("unknown", ""): 59 | return False 60 | elif address.count(".") == 3: # assume IPv4 61 | if address.startswith("::ffff:"): 62 | address = address[7:] 63 | if hasattr(socket, "inet_aton"): # try validate using the OS 64 | try: 65 | socket.inet_aton(address) 66 | return True 67 | except socket.error: # invalid address 68 | return False 69 | else: # try validate using Regex 70 | match = _re_ipv4.match(address) 71 | if match and all(0 <= int(match.group(i)) < 256 for i in (1, 2, 3, 4)): 72 | return True 73 | return False 74 | elif hasattr(socket, "inet_pton"): # assume IPv6, try using the OS 75 | try: 76 | socket.inet_pton(socket.AF_INET6, address) 77 | return True 78 | except socket.error: # invalid address 79 | return False 80 | else: # do not know what to do? assume it is a valid address 81 | return True 82 | 83 | 84 | def read_file(filename, mode="r"): 85 | # returns content from filename, making sure to close the file on exit. 86 | f = open(filename, mode) 87 | try: 88 | return f.read() 89 | finally: 90 | f.close() 91 | 92 | 93 | def write_file(filename, value, mode="w"): 94 | # writes to filename, making sure to close the file on exit. 95 | f = open(filename, mode) 96 | try: 97 | return f.write(value) 98 | finally: 99 | f.close() 100 | 101 | 102 | def dict_to_sdict(obj): 103 | #: convert dict and nested dicts to sdict 104 | if isinstance(obj, dict) and not isinstance(obj, sdict): 105 | for k in obj: 106 | obj[k] = dict_to_sdict(obj[k]) 107 | return sdict(obj) 108 | return obj 109 | -------------------------------------------------------------------------------- /emmett/validators/process.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.validators.process 4 | ------------------------- 5 | 6 | Validators that transform values. 7 | 8 | Ported from the original validators of web2py (http://www.web2py.com) 9 | 10 | :copyright: (c) by Massimo Di Pierro 11 | :license: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 12 | """ 13 | 14 | import re 15 | import unicodedata 16 | 17 | # TODO: check unicode conversions 18 | from .._shortcuts import to_unicode 19 | from .basic import Validator 20 | from .helpers import LazyCrypt, translate 21 | 22 | 23 | class Cleanup(Validator): 24 | rule = re.compile(r"[^\x09\x0a\x0d\x20-\x7e]") 25 | 26 | def __init__(self, regex=None, message=None): 27 | super().__init__(message=message) 28 | self.regex = self.rule if regex is None else re.compile(regex) 29 | 30 | def __call__(self, value): 31 | v = self.regex.sub("", (to_unicode(value) or "").strip()) 32 | return v, None 33 | 34 | 35 | class Lower(Validator): 36 | def __call__(self, value): 37 | if value is None: 38 | return (value, None) 39 | return (to_unicode(value).lower(), None) 40 | 41 | 42 | class Upper(Validator): 43 | def __call__(self, value): 44 | if value is None: 45 | return (value, None) 46 | return (to_unicode(value).upper(), None) 47 | 48 | 49 | class Urlify(Validator): 50 | message = "Not convertible to url" 51 | 52 | def __init__(self, maxlen=80, check=False, keep_underscores=False, message=None): 53 | super().__init__(message=message) 54 | self.maxlen = maxlen 55 | self.check = check 56 | self.message = message 57 | self.keep_underscores = keep_underscores 58 | 59 | def __call__(self, value): 60 | if self.check and value != self._urlify(value): 61 | return value, translate(self.message) 62 | return self._urlify(value), None 63 | 64 | def _urlify(self, s): 65 | """ 66 | Converts incoming string to a simplified ASCII subset. 67 | if (keep_underscores): underscores are retained in the string 68 | else: underscores are translated to hyphens (default) 69 | """ 70 | s = to_unicode(s) 71 | # to lowercase 72 | s = s.lower() 73 | # replace special characters 74 | s = unicodedata.normalize("NFKD", s) 75 | # encode as ASCII 76 | s = s.encode("ascii", "ignore").decode("ascii") 77 | # strip html entities 78 | s = re.sub(r"&\w+?;", "", s) 79 | if self.keep_underscores: 80 | # whitespace to hypens 81 | s = re.sub(r"\s+", "-", s) 82 | # strip all but alphanumeric/underscore/hyphen 83 | s = re.sub(r"[^\w\-]", "", s) 84 | else: 85 | # whitespace & underscores to hyphens 86 | s = re.sub(r"[\s_]+", "-", s) 87 | # strip all but alphanumeric/hyphen 88 | s = re.sub(r"[^a-z0-9\-]", "", s) 89 | # collapse strings of hyphens 90 | s = re.sub(r"[-_][-_]+", "-", s) 91 | # remove leading and trailing hyphens 92 | s = s.strip(r"-") 93 | # enforce maximum length 94 | return s[: self.maxlen] 95 | 96 | 97 | class Crypt(Validator): 98 | """ 99 | encodes the value on validation with a digest. 100 | 101 | If no arguments are provided Crypt uses the MD5 algorithm. 102 | If the key argument is provided the HMAC+MD5 algorithm is used. 103 | If the digest_alg is specified this is used to replace the 104 | MD5 with, for example, SHA512. The digest_alg can be 105 | the name of a hashlib algorithm as a string or the algorithm itself. 106 | 107 | min_length is the minimal password length (default 4) 108 | error_message is the message if password is too short 109 | 110 | Notice that an empty password is accepted but invalid. It will not allow 111 | login back. Stores junk as hashed password. 112 | 113 | Specify an algorithm or by default we will use sha512. 114 | 115 | Typical available algorithms: 116 | md5, sha1, sha224, sha256, sha384, sha512 117 | 118 | If salt, it hashes a password with a salt. 119 | If salt is True, this method will automatically generate one. 120 | Either case it returns an encrypted password string with format: 121 | 122 | $$ 123 | 124 | Important: hashed password is returned as a LazyCrypt object and computed 125 | only if needed. The LasyCrypt object also knows how to compare itself with 126 | an existing salted password 127 | """ 128 | 129 | def __init__(self, key=None, algorithm="pbkdf2(1000,20,sha512)", salt=True, message=None): 130 | super().__init__(message=message) 131 | self.key = key 132 | self.digest_alg = algorithm 133 | self.salt = salt 134 | 135 | def __call__(self, value): 136 | if getattr(value, "_emt_field_hashed_contents_", False): 137 | return value, None 138 | crypt = LazyCrypt(self, value) 139 | if isinstance(value, LazyCrypt) and value == crypt: 140 | return value, None 141 | return crypt, None 142 | -------------------------------------------------------------------------------- /emmett/wrappers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emmett-framework/emmett/8f206515d0a7b01bf399656383c0b0019ce90065/emmett/wrappers/__init__.py -------------------------------------------------------------------------------- /emmett/wrappers/request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.wrappers.request 4 | ----------------------- 5 | 6 | Provides http request wrappers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import pendulum 13 | from emmett_core.http.wrappers.request import Request as _Request 14 | from emmett_core.utils import cachedprop 15 | 16 | 17 | class Request(_Request): 18 | __slots__ = [] 19 | 20 | # method: str 21 | 22 | @cachedprop 23 | def now(self) -> pendulum.DateTime: 24 | return pendulum.instance(self._now) 25 | 26 | @cachedprop 27 | def now_local(self) -> pendulum.DateTime: 28 | return self.now.in_timezone(pendulum.local_timezone()) # type: ignore 29 | -------------------------------------------------------------------------------- /emmett/wrappers/response.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.wrappers.response 4 | ------------------------ 5 | 6 | Provides response wrappers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | import os 13 | import re 14 | from typing import Any 15 | 16 | from emmett_core.http.response import HTTPFileResponse, HTTPResponse 17 | from emmett_core.http.wrappers.response import Response as _Response 18 | from emmett_core.utils import cachedprop 19 | from pydal.exceptions import NotAuthorizedException, NotFoundException 20 | 21 | from ..ctx import current 22 | from ..datastructures import sdict 23 | from ..helpers import abort, get_flashed_messages 24 | from ..html import htmlescape 25 | 26 | 27 | _re_dbstream = re.compile(r"(?P.*?)\.(?P.*?)\..*") 28 | 29 | 30 | class ResponseMixin: 31 | @cachedprop 32 | def meta(self) -> sdict[str, Any]: 33 | return sdict() 34 | 35 | @cachedprop 36 | def meta_prop(self) -> sdict[str, Any]: 37 | return sdict() 38 | 39 | def alerts(self, **kwargs): 40 | return get_flashed_messages(**kwargs) 41 | 42 | def _meta_tmpl(self): 43 | return [(key, htmlescape(val)) for key, val in self.meta.items()] 44 | 45 | def _meta_tmpl_prop(self): 46 | return [(key, htmlescape(val)) for key, val in self.meta_prop.items()] 47 | 48 | def wrap_file(self, path) -> HTTPFileResponse: 49 | path = os.path.join(current.app.root_path, path) 50 | return super().wrap_file(path) 51 | 52 | def wrap_dbfile(self, db, name: str) -> HTTPResponse: 53 | items = _re_dbstream.match(name) 54 | if not items: 55 | abort(404) 56 | table_name, field_name = items.group("table"), items.group("field") 57 | try: 58 | field = db[table_name][field_name] 59 | except AttributeError: 60 | abort(404) 61 | try: 62 | filename, path_or_stream = field.retrieve(name, nameonly=True) 63 | except NotAuthorizedException: 64 | abort(403) 65 | except NotFoundException: 66 | abort(404) 67 | except IOError: 68 | abort(404) 69 | if isinstance(path_or_stream, str): 70 | return self.wrap_file(path_or_stream) 71 | return self.wrap_io(path_or_stream) 72 | 73 | 74 | class Response(ResponseMixin, _Response): 75 | __slots__ = [] 76 | -------------------------------------------------------------------------------- /emmett/wrappers/websocket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | emmett.wrappers.websocket 4 | ------------------------- 5 | 6 | Provides http websocket wrappers. 7 | 8 | :copyright: 2014 Giovanni Barillari 9 | :license: BSD-3-Clause 10 | """ 11 | 12 | from emmett_core.http.wrappers.websocket import Websocket as Websocket 13 | -------------------------------------------------------------------------------- /examples/bloggy/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from emmett import App, session, now, url, redirect, abort 4 | from emmett.orm import Database, Model, Field, belongs_to, has_many 5 | from emmett.tools import requires 6 | from emmett.tools.auth import Auth, AuthUser 7 | from emmett.sessions import SessionManager 8 | 9 | 10 | app = App(__name__) 11 | app.config.auth.single_template = True 12 | app.config.auth.registration_verification = False 13 | app.config.auth.hmac_key = "november.5.1955" 14 | 15 | 16 | #: define models 17 | class User(AuthUser): 18 | # will create "auth_user" table and groups/permissions ones 19 | has_many('posts', 'comments') 20 | 21 | 22 | class Post(Model): 23 | belongs_to('user') 24 | has_many('comments') 25 | 26 | title = Field() 27 | text = Field.text() 28 | date = Field.datetime() 29 | 30 | default_values = { 31 | 'user': lambda: session.auth.user.id, 32 | 'date': now 33 | } 34 | validation = { 35 | 'title': {'presence': True}, 36 | 'text': {'presence': True} 37 | } 38 | fields_rw = { 39 | 'user': False, 40 | 'date': False 41 | } 42 | 43 | 44 | class Comment(Model): 45 | belongs_to('user', 'post') 46 | 47 | text = Field.text() 48 | date = Field.datetime() 49 | 50 | default_values = { 51 | 'user': lambda: session.auth.user.id, 52 | 'date': now 53 | } 54 | validation = { 55 | 'text': {'presence': True} 56 | } 57 | fields_rw = { 58 | 'user': False, 59 | 'post': False, 60 | 'date': False 61 | } 62 | 63 | 64 | #: init db and auth 65 | db = Database(app) 66 | auth = Auth(app, db, user_model=User) 67 | db.define_models(Post, Comment) 68 | 69 | 70 | #: setup helping function 71 | def setup_admin(): 72 | with db.connection(): 73 | # create the user 74 | user = User.create( 75 | email="doc@emmettbrown.com", 76 | first_name="Emmett", 77 | last_name="Brown", 78 | password="fluxcapacitor" 79 | ) 80 | # create an admin group 81 | admins = auth.create_group("admin") 82 | # add user to admins group 83 | auth.add_membership(admins, user.id) 84 | db.commit() 85 | 86 | 87 | @app.command('setup') 88 | def setup(): 89 | setup_admin() 90 | 91 | 92 | #: pipeline 93 | app.pipeline = [ 94 | SessionManager.cookies('GreatScott'), 95 | db.pipe, 96 | auth.pipe 97 | ] 98 | 99 | 100 | #: exposing functions 101 | @app.route("/") 102 | async def index(): 103 | posts = Post.all().select(orderby=~Post.date) 104 | return dict(posts=posts) 105 | 106 | 107 | @app.route("/post/") 108 | async def one(pid): 109 | def _validate_comment(form): 110 | # manually set post id in comment form 111 | form.params.post = pid 112 | # get post and return 404 if doesn't exist 113 | post = Post.get(pid) 114 | if not post: 115 | abort(404) 116 | # get comments 117 | comments = post.comments(orderby=~Comment.date) 118 | # and create a form for commenting if the user is logged in 119 | if session.auth: 120 | form = await Comment.form(onvalidation=_validate_comment) 121 | if form.accepted: 122 | redirect(url('one', pid)) 123 | return locals() 124 | 125 | 126 | @app.route("/new") 127 | @requires(lambda: auth.has_membership('admin'), url('index')) 128 | async def new_post(): 129 | form = await Post.form() 130 | if form.accepted: 131 | redirect(url('one', form.params.id)) 132 | return dict(form=form) 133 | 134 | 135 | auth_routes = auth.module(__name__) 136 | -------------------------------------------------------------------------------- /examples/bloggy/migrations/9d6518b3cdc2_first_migration.py: -------------------------------------------------------------------------------- 1 | """First migration 2 | 3 | Migration ID: 9d6518b3cdc2 4 | Revises: 5 | Creation Date: 2020-04-14 17:42:00.087202 6 | 7 | """ 8 | 9 | from emmett.orm import migrations 10 | 11 | 12 | class Migration(migrations.Migration): 13 | revision = '9d6518b3cdc2' 14 | revises = None 15 | 16 | def up(self): 17 | self.create_table( 18 | 'users', 19 | migrations.Column('id', 'id'), 20 | migrations.Column('created_at', 'datetime'), 21 | migrations.Column('updated_at', 'datetime'), 22 | migrations.Column('email', 'string', length=255), 23 | migrations.Column('password', 'password', length=512), 24 | migrations.Column('registration_key', 'string', default='', length=512), 25 | migrations.Column('reset_password_key', 'string', default='', length=512), 26 | migrations.Column('registration_id', 'string', default='', length=512), 27 | migrations.Column('first_name', 'string', notnull=True, length=128), 28 | migrations.Column('last_name', 'string', notnull=True, length=128)) 29 | self.create_table( 30 | 'auth_groups', 31 | migrations.Column('id', 'id'), 32 | migrations.Column('created_at', 'datetime'), 33 | migrations.Column('updated_at', 'datetime'), 34 | migrations.Column('role', 'string', default='', length=255), 35 | migrations.Column('description', 'text')) 36 | self.create_table( 37 | 'auth_memberships', 38 | migrations.Column('id', 'id'), 39 | migrations.Column('created_at', 'datetime'), 40 | migrations.Column('updated_at', 'datetime'), 41 | migrations.Column('user', 'reference users', ondelete='CASCADE'), 42 | migrations.Column('auth_group', 'reference auth_groups', ondelete='CASCADE')) 43 | self.create_table( 44 | 'auth_permissions', 45 | migrations.Column('id', 'id'), 46 | migrations.Column('created_at', 'datetime'), 47 | migrations.Column('updated_at', 'datetime'), 48 | migrations.Column('name', 'string', default='default', notnull=True, length=512), 49 | migrations.Column('table_name', 'string', length=512), 50 | migrations.Column('record_id', 'integer', default=0), 51 | migrations.Column('auth_group', 'reference auth_groups', ondelete='CASCADE')) 52 | self.create_table( 53 | 'auth_events', 54 | migrations.Column('id', 'id'), 55 | migrations.Column('created_at', 'datetime'), 56 | migrations.Column('updated_at', 'datetime'), 57 | migrations.Column('client_ip', 'string', length=512), 58 | migrations.Column('origin', 'string', default='auth', notnull=True, length=512), 59 | migrations.Column('description', 'text', default='', notnull=True), 60 | migrations.Column('user', 'reference users', ondelete='CASCADE')) 61 | self.create_table( 62 | 'posts', 63 | migrations.Column('id', 'id'), 64 | migrations.Column('title', 'string', length=512), 65 | migrations.Column('text', 'text'), 66 | migrations.Column('date', 'datetime'), 67 | migrations.Column('user', 'reference users', ondelete='CASCADE')) 68 | self.create_table( 69 | 'comments', 70 | migrations.Column('id', 'id'), 71 | migrations.Column('text', 'text'), 72 | migrations.Column('date', 'datetime'), 73 | migrations.Column('user', 'reference users', ondelete='CASCADE'), 74 | migrations.Column('post', 'reference posts', ondelete='CASCADE')) 75 | 76 | def down(self): 77 | self.drop_table('comments') 78 | self.drop_table('posts') 79 | self.drop_table('auth_events') 80 | self.drop_table('auth_permissions') 81 | self.drop_table('auth_memberships') 82 | self.drop_table('auth_groups') 83 | self.drop_table('users') 84 | -------------------------------------------------------------------------------- /examples/bloggy/static/style.css: -------------------------------------------------------------------------------- 1 | body { font-family: sans-serif; background: #eee; } 2 | a, h1, h2 { color: #377ba8; } 3 | h1, h2, h4, h5 { font-family: 'Georgia', serif; } 4 | h1 { border-bottom: 2px solid #eee; } 5 | h2 { font-size: 1.2em; } 6 | 7 | .page { margin: 2em auto; width: 35em; border: 5px solid #ccc; 8 | padding: 0.8em; background: white; } 9 | .title { text-decoration: none; } 10 | .posts { list-style: none; margin: 0; padding: 0; } 11 | .posts li { margin: 0.8em 1.2em; } 12 | .posts li h2 { margin-left: -1em; } 13 | .posts li hr { margin-left: -0.8em; } 14 | .nav { text-align: right; font-size: 0.8em; padding: 0.3em; 15 | margin-bottom: 1em; background: #fafafa; } 16 | -------------------------------------------------------------------------------- /examples/bloggy/templates/auth/auth.html: -------------------------------------------------------------------------------- 1 | {{extend 'layout.html'}} 2 | 3 |

Account

4 | 5 | {{for flash in current.response.alerts(category_filter='auth'):}} 6 |
{{=flash}}
7 | {{pass}} 8 | 9 | {{=form}} 10 | -------------------------------------------------------------------------------- /examples/bloggy/templates/index.html: -------------------------------------------------------------------------------- 1 | {{extend 'layout.html'}} 2 | 3 | Create a new post 4 |
    5 | {{for post in posts:}} 6 |
  • 7 |

    {{=post.title}}

    8 | Read more 9 |
    10 |
  • 11 | {{pass}} 12 | {{if not posts:}} 13 |
  • No posts here so far.
  • 14 | {{pass}} 15 |
16 | -------------------------------------------------------------------------------- /examples/bloggy/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bloggy 5 | {{include_meta}} 6 | {{include_helpers}} 7 | {{include_static 'style.css'}} 8 | 9 | 10 |
11 |

Bloggy

12 | 19 | {{block main}} 20 | {{include}} 21 | {{end}} 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/bloggy/templates/new_post.html: -------------------------------------------------------------------------------- 1 | {{extend 'layout.html'}} 2 | 3 |

Create a new post

4 | {{=form}} 5 | -------------------------------------------------------------------------------- /examples/bloggy/templates/one.html: -------------------------------------------------------------------------------- 1 | {{extend 'layout.html'}} 2 | 3 |

{{=post.title}}

4 | {{=post.text}} 5 |
6 |
7 |

Comments

8 | {{if current.session.auth:}} 9 |
Write a comment:
10 | {{=form}} 11 | {{pass}} 12 |
    13 | {{for comment in comments:}} 14 |
  • 15 | {{=comment.text}} 16 |
    17 | by {{=comment.user.first_name}} on {{=comment.date}} 18 |
  • 19 | {{pass}} 20 | {{if not comments:}} 21 |
  • No comments here so far.
  • 22 | {{pass}} 23 |
24 | -------------------------------------------------------------------------------- /examples/bloggy/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from emmett.orm.migrations.utils import generate_runtime_migration 6 | from bloggy import app, db, User, auth, setup_admin 7 | 8 | 9 | @pytest.fixture() 10 | def client(): 11 | return app.test_client() 12 | 13 | 14 | @pytest.fixture(scope='module', autouse=True) 15 | def _prepare_db(request): 16 | with db.connection(): 17 | migration = generate_runtime_migration(db) 18 | migration.up() 19 | setup_admin() 20 | yield 21 | with db.connection(): 22 | User.all().delete() 23 | auth.delete_group('admin') 24 | migration.down() 25 | 26 | 27 | @pytest.fixture(scope='module') 28 | def logged_client(): 29 | c = app.test_client() 30 | with c.get('/auth/login').context as ctx: 31 | c.post('/auth/login', data={ 32 | 'email': 'doc@emmettbrown.com', 33 | 'password': 'fluxcapacitor', 34 | '_csrf_token': list(ctx.session._csrf)[-1] 35 | }, follow_redirects=True) 36 | return c 37 | 38 | 39 | def test_empty_db(client): 40 | r = client.get('/') 41 | assert 'No posts here so far' in r.data 42 | 43 | 44 | def test_login(logged_client): 45 | r = logged_client.get('/') 46 | assert r.context.session.auth.user is not None 47 | 48 | 49 | def test_no_admin_access(client): 50 | r = client.get('/new') 51 | assert r.context.response.status == 303 52 | 53 | 54 | def test_admin_access(logged_client): 55 | r = logged_client.get('/new') 56 | assert r.context.response.status == 200 57 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "emmett" 7 | version = "2.7.0" 8 | description = "The web framework for inventors" 9 | readme = "README.md" 10 | license = "BSD-3-Clause" 11 | requires-python = ">=3.9" 12 | 13 | authors = [ 14 | { name = "Giovanni Barillari", email = "g@baro.dev" } 15 | ] 16 | 17 | keywords = ["web", "asyncio"] 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Environment :: Web Environment", 21 | "Framework :: AsyncIO", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: BSD License", 24 | "Operating System :: OS Independent", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3.13", 31 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 32 | "Topic :: Software Development :: Libraries :: Python Modules" 33 | ] 34 | 35 | dependencies = [ 36 | "click>=6.0", 37 | "emmett-core[granian,rapidjson]~=1.3.0", 38 | "emmett-pydal==17.3.2", 39 | "pendulum~=3.0", 40 | "pyyaml~=6.0", 41 | "renoir~=1.6", 42 | "severus~=1.1", 43 | ] 44 | 45 | [project.optional-dependencies] 46 | orjson = ["orjson~=3.10"] 47 | rloop = ['rloop~=0.1; sys_platform != "win32"'] 48 | uvloop = ['uvloop>=0.18.0; sys_platform != "win32" and platform_python_implementation == "CPython"'] 49 | 50 | [project.urls] 51 | Homepage = 'https://emmett.sh' 52 | Documentation = 'https://emmett.sh/docs' 53 | Funding = 'https://github.com/sponsors/gi0baro' 54 | Source = 'https://github.com/emmett-framework/emmett' 55 | Issues = 'https://github.com/emmett-framework/emmett/issues' 56 | 57 | [project.scripts] 58 | emmett = "emmett.cli:main" 59 | 60 | [tool.hatch.build.targets.sdist] 61 | include = [ 62 | '/README.md', 63 | '/CHANGES.md', 64 | '/LICENSE', 65 | '/docs', 66 | '/emmett', 67 | '/tests', 68 | ] 69 | 70 | [tool.ruff] 71 | line-length = 120 72 | 73 | [tool.ruff.format] 74 | quote-style = 'double' 75 | 76 | [tool.ruff.lint] 77 | extend-select = [ 78 | # E and F are enabled by default 79 | 'B', # flake8-bugbear 80 | 'C4', # flake8-comprehensions 81 | 'C90', # mccabe 82 | 'I', # isort 83 | 'N', # pep8-naming 84 | 'Q', # flake8-quotes 85 | 'RUF100', # ruff (unused noqa) 86 | 'S', # flake8-bandit 87 | 'W', # pycodestyle 88 | ] 89 | extend-ignore = [ 90 | 'B006', # mutable function args are fine 91 | 'B008', # function calls in args defaults are fine 92 | 'B009', # getattr with constants is fine 93 | 'B034', # re.split won't confuse us 94 | 'B904', # rising without from is fine 95 | 'E731', # assigning lambdas is fine 96 | 'F403', # import * is fine 97 | 'N801', # leave to us class naming 98 | 'N802', # leave to us method naming 99 | 'N806', # leave to us var naming 100 | 'N811', # leave to us var naming 101 | 'N814', # leave to us var naming 102 | 'N818', # leave to us exceptions naming 103 | 'S101', # assert is fine 104 | 'S104', # leave to us security 105 | 'S105', # leave to us security 106 | 'S106', # leave to us security 107 | 'S107', # leave to us security 108 | 'S110', # pass on exceptions is fine 109 | 'S301', # leave to us security 110 | 'S324', # leave to us security 111 | ] 112 | mccabe = { max-complexity = 44 } 113 | 114 | [tool.ruff.lint.isort] 115 | combine-as-imports = true 116 | lines-after-imports = 2 117 | known-first-party = ['emmett', 'tests'] 118 | 119 | [tool.ruff.lint.per-file-ignores] 120 | 'emmett/__init__.py' = ['F401'] 121 | 'emmett/http.py' = ['F401'] 122 | 'emmett/orm/__init__.py' = ['F401'] 123 | 'emmett/orm/engines/__init__.py' = ['F401'] 124 | 'emmett/orm/migrations/__init__.py' = ['F401'] 125 | 'emmett/orm/migrations/revisions.py' = ['B018'] 126 | 'emmett/tools/__init__.py' = ['F401'] 127 | 'emmett/tools/auth/__init__.py' = ['F401'] 128 | 'emmett/validators/__init__.py' = ['F401'] 129 | 'tests/**' = ['B017', 'B018', 'E711', 'E712', 'E741', 'F841', 'S110', 'S501'] 130 | 131 | [tool.pytest.ini_options] 132 | asyncio_mode = 'auto' 133 | 134 | [tool.uv] 135 | dev-dependencies = [ 136 | "ipaddress>=1.0", 137 | "pytest>=7.1", 138 | "pytest-asyncio>=0.15", 139 | "psycopg2-binary~=2.9", 140 | "ruff~=0.11.0", 141 | ] 142 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.helpers 4 | ------------- 5 | 6 | Tests helpers 7 | """ 8 | 9 | from contextlib import contextmanager 10 | 11 | from emmett_core.protocols.rsgi.test_client.scope import ScopeBuilder 12 | 13 | from emmett.ctx import RequestContext, WSContext, current 14 | from emmett.datastructures import sdict 15 | from emmett.rsgi.wrappers import Request, Response, Websocket 16 | from emmett.serializers import Serializers 17 | 18 | 19 | json_dump = Serializers.get_for("json") 20 | 21 | 22 | class FakeRequestContext(RequestContext): 23 | def __init__(self, app, scope): 24 | self.app = app 25 | self.request = Request(scope, scope.path, None, None) 26 | self.response = Response(None) 27 | self.session = None 28 | 29 | 30 | class FakeWSTransport: 31 | def __init__(self): 32 | self._send_storage = [] 33 | 34 | async def receive(self): 35 | return json_dump({"foo": "bar"}) 36 | 37 | async def send_str(self, data): 38 | self._send_storage.append(data) 39 | 40 | async def send_bytes(self, data): 41 | self._send_storage.append(data) 42 | 43 | 44 | class FakeWsProto: 45 | def __init__(self): 46 | self.transport = None 47 | 48 | async def init(self): 49 | self.transport = FakeWSTransport() 50 | 51 | async def receive(self): 52 | return sdict(data=await self.transport.receive()) 53 | 54 | def close(self): 55 | pass 56 | 57 | 58 | class FakeWSContext(WSContext): 59 | def __init__(self, app, scope): 60 | self.app = app 61 | self._proto = FakeWsProto() 62 | self.websocket = Websocket(scope, scope.path, self._proto) 63 | self._receive_storage = [] 64 | 65 | @property 66 | def _send_storage(self): 67 | return self._proto.transport._send_storage 68 | 69 | 70 | @contextmanager 71 | def current_ctx(path, app=None): 72 | builder = ScopeBuilder(path) 73 | token = current._init_(FakeRequestContext(app, builder.get_data()[0])) 74 | yield current 75 | current._close_(token) 76 | 77 | 78 | @contextmanager 79 | def ws_ctx(path, app=None): 80 | builder = ScopeBuilder(path) 81 | scope_data = builder.get_data()[0] 82 | scope_data.proto = "ws" 83 | token = current._init_(FakeWSContext(app, scope_data)) 84 | yield current 85 | current._close_(token) 86 | -------------------------------------------------------------------------------- /tests/languages/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "partly cloudy": "teilweise bewölkt" 3 | } 4 | -------------------------------------------------------------------------------- /tests/languages/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "partly cloudy": "nuvolosità variabile" 3 | } 4 | -------------------------------------------------------------------------------- /tests/languages/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "partly cloudy": "переменная облачность" 3 | } 4 | -------------------------------------------------------------------------------- /tests/templates/auth/auth.html: -------------------------------------------------------------------------------- 1 | {{for flash in current.response.alerts(category_filter='auth'):}} 2 |
{{=flash}}
3 | {{pass}} 4 | 5 | {{=form}} 6 | -------------------------------------------------------------------------------- /tests/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test 5 | {{include_meta}} 6 | {{include_helpers}} 7 | {{include_static 'style.css'}} 8 | 9 | 10 |
11 |

Test

12 | 15 | {{block main}} 16 | {{include}} 17 | {{end}} 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/templates/test.html: -------------------------------------------------------------------------------- 1 | {{extend 'layout.html'}} 2 | 3 |
    4 | {{for post in posts:}} 5 |
  • 6 |

    {{=post['title']}}

    7 |
    8 |
  • 9 | {{pass}} 10 |
11 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.cache 4 | ----------- 5 | 6 | Test Emmett cache module 7 | """ 8 | 9 | import pytest 10 | 11 | from emmett import App 12 | from emmett.cache import DiskCache 13 | 14 | 15 | async def _await_2(): 16 | return 2 17 | 18 | 19 | async def _await_3(): 20 | return 3 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_diskcache(): 25 | App(__name__) 26 | 27 | disk_cache = DiskCache() 28 | assert disk_cache._threshold == 500 29 | 30 | assert disk_cache("test", lambda: 2) == 2 31 | assert disk_cache("test", lambda: 3, 300) == 2 32 | 33 | assert await disk_cache("test_loop", _await_2) == 2 34 | assert await disk_cache("test_loop", _await_3, 300) == 2 35 | 36 | disk_cache.set("test", 3) 37 | assert disk_cache.get("test") == 3 38 | 39 | disk_cache.set("test", 4, 300) 40 | assert disk_cache.get("test") == 4 41 | 42 | disk_cache.clear() 43 | assert disk_cache.get("test") is None 44 | -------------------------------------------------------------------------------- /tests/test_logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.logger 4 | ------------ 5 | 6 | Test Emmett logging module 7 | """ 8 | 9 | import logging 10 | 11 | from emmett_core import log as logger 12 | 13 | from emmett import App, sdict 14 | 15 | 16 | def _call_create_logger(app): 17 | return logger.create_logger(app) 18 | 19 | 20 | def test_user_assign_valid_level(): 21 | app = App(__name__) 22 | app.config.logging.pytest = sdict(level="info") 23 | result = _call_create_logger(app) 24 | assert result.handlers[-1].level == logging.INFO 25 | 26 | 27 | def test_user_assign_invaild_level(): 28 | app = App(__name__) 29 | app.config.logging.pytest = sdict(level="invalid") 30 | result = _call_create_logger(app) 31 | assert result.handlers[-1].level == logging.WARNING 32 | 33 | 34 | def test_user_no_assign_level(): 35 | app = App(__name__) 36 | app.config.logging.pytest = sdict() 37 | result = _call_create_logger(app) 38 | assert result.handlers[-1].level == logging.WARNING 39 | -------------------------------------------------------------------------------- /tests/test_orm_connections.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.orm_connections 4 | --------------------- 5 | 6 | Test pyDAL connection implementation over Emmett. 7 | """ 8 | 9 | import pytest 10 | 11 | from emmett import App, sdict 12 | from emmett.orm import Database 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def db(): 17 | app = App(__name__) 18 | db = Database(app, config=sdict(uri="sqlite:memory", auto_migrate=True, auto_connect=False)) 19 | return db 20 | 21 | 22 | def test_connection_ctx_sync(db): 23 | assert not db._adapter.connection 24 | 25 | with db.connection(): 26 | assert db._adapter.connection 27 | 28 | assert not db._adapter.connection 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_connection_ctx_loop(db): 33 | assert not db._adapter.connection 34 | 35 | async with db.connection(): 36 | assert db._adapter.connection 37 | 38 | assert not db._adapter.connection 39 | -------------------------------------------------------------------------------- /tests/test_orm_transactions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.orm_transactions 4 | ---------------------- 5 | 6 | Test pyDAL transactions implementation over Emmett. 7 | """ 8 | 9 | import pytest 10 | 11 | from emmett import App, sdict 12 | from emmett.orm import Database, Field, Model 13 | 14 | 15 | class Register(Model): 16 | value = Field.int() 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def db(): 21 | app = App(__name__) 22 | db = Database(app, config=sdict(uri="sqlite:memory", auto_migrate=True, auto_connect=True)) 23 | db.define_models(Register) 24 | return db 25 | 26 | 27 | @pytest.fixture(scope="function") 28 | def cleanup(request, db): 29 | def teardown(): 30 | Register.all().delete() 31 | db.commit() 32 | 33 | request.addfinalizer(teardown) 34 | 35 | 36 | def _save(*vals): 37 | for val in vals: 38 | Register.create(value=val) 39 | 40 | 41 | def _values_in_register(*vals): 42 | db_vals = Register.all().select(orderby=Register.value).column("value") 43 | return db_vals == list(vals) 44 | 45 | 46 | def test_transactions(db, cleanup): 47 | adapter = db._adapter 48 | 49 | assert adapter.in_transaction() 50 | 51 | _save(1) 52 | db.commit() 53 | assert _values_in_register(1) 54 | 55 | _save(2) 56 | db.rollback() 57 | _save(3) 58 | with db.atomic(): 59 | _save(4) 60 | with db.atomic() as sp2: 61 | _save(5) 62 | sp2.rollback() 63 | with db.atomic(): 64 | _save(6) 65 | with db.atomic() as sp4: 66 | _save(7) 67 | with db.atomic(): 68 | _save(8) 69 | assert _values_in_register(1, 3, 4, 6, 7, 8) 70 | sp4.rollback() 71 | assert _values_in_register(1, 3, 4, 6) 72 | db.commit() 73 | assert _values_in_register(1, 3, 4, 6) 74 | 75 | 76 | def _commit_rollback(db): 77 | _save(1) 78 | db.commit() 79 | _save(2) 80 | db.rollback() 81 | assert _values_in_register(1) 82 | 83 | _save(3) 84 | db.rollback() 85 | _save(4) 86 | db.commit() 87 | assert _values_in_register(1, 4) 88 | 89 | 90 | def test_commit_rollback(db, cleanup): 91 | _commit_rollback(db) 92 | 93 | 94 | def test_commit_rollback_nested(db, cleanup): 95 | with db.atomic(): 96 | _commit_rollback(db) 97 | assert _values_in_register(1, 4) 98 | 99 | 100 | def test_nested_transaction_obj(db, cleanup): 101 | assert _values_in_register() 102 | 103 | _save(1) 104 | with db.transaction() as txn: 105 | _save(2) 106 | txn.rollback() 107 | assert _values_in_register() 108 | _save(3) 109 | db.commit() 110 | assert _values_in_register(3) 111 | 112 | 113 | def test_savepoint_commit(db, cleanup): 114 | _save(1) 115 | db.rollback() 116 | 117 | _save(2) 118 | db.commit() 119 | 120 | with db.atomic() as sp: 121 | _save(3) 122 | sp.rollback() 123 | _save(4) 124 | sp.commit() 125 | 126 | assert _values_in_register(2, 4) 127 | 128 | 129 | def text_atomic_exception(db, cleanup): 130 | def will_fail(): 131 | with db.atomic(): 132 | _save(1) 133 | _save(None) 134 | 135 | with pytest.raises(Exception): 136 | will_fail() 137 | assert _values_in_register() 138 | 139 | def user_error(): 140 | with db.atomic(): 141 | _save(2) 142 | raise ValueError 143 | 144 | with pytest.raises(ValueError): 145 | user_error() 146 | assert _values_in_register() 147 | -------------------------------------------------------------------------------- /tests/test_templates.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.templates 4 | --------------- 5 | 6 | Test Emmett templating module 7 | """ 8 | 9 | import pytest 10 | from helpers import current_ctx 11 | 12 | from emmett import App 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def app(): 17 | app = App(__name__) 18 | app.config.templates_escape = "all" 19 | app.config.templates_adjust_indent = False 20 | app.config.templates_auto_reload = True 21 | return app 22 | 23 | 24 | def test_helpers(app): 25 | templater = app.templater 26 | r = templater._render(source="{{ include_helpers }}") 27 | assert ( 28 | r 29 | == '' 31 | + '' 33 | ) 34 | 35 | 36 | def test_meta(app): 37 | with current_ctx("/", app) as ctx: 38 | ctx.response.meta.foo = "bar" 39 | ctx.response.meta_prop.foo = "bar" 40 | templater = app.templater 41 | r = templater._render(source="{{ include_meta }}", context={"current": ctx}) 42 | assert r == '' + '' 43 | 44 | 45 | def test_static(app): 46 | templater = app.templater 47 | s = "{{include_static 'foo.js'}}\n{{include_static 'bar.css'}}" 48 | r = templater._render(source=s) 49 | assert ( 50 | r 51 | == '\n' 54 | ) 55 | 56 | 57 | rendered_value = """ 58 | 59 | 60 | 61 | Test 62 | {helpers} 63 | 64 | 65 | 66 |
67 |

Test

68 | 71 |
    72 |
  • 73 |

    foo

    74 |
    75 |
  • 76 |
  • 77 |

    bar

    78 |
    79 |
  • 80 |
81 |
82 | 83 | """.format( 84 | helpers="".join( 85 | [ 86 | f'' 87 | for name in ["jquery.min.js", "helpers.js"] 88 | ] 89 | ) 90 | ) 91 | 92 | 93 | def test_render(app): 94 | with current_ctx("/", app) as ctx: 95 | ctx.language = "it" 96 | r = app.templater.render("test.html", {"current": ctx, "posts": [{"title": "foo"}, {"title": "bar"}]}) 97 | assert "\n".join([l.strip() for l in r.splitlines() if l.strip()]) == "\n".join( 98 | [l.strip() for l in rendered_value[1:].splitlines()] 99 | ) 100 | -------------------------------------------------------------------------------- /tests/test_translator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.translator 4 | ---------------- 5 | 6 | Test Emmett translator module 7 | """ 8 | 9 | import pytest 10 | 11 | from emmett import App 12 | from emmett.ctx import current 13 | from emmett.locals import T 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def app(): 18 | return App(__name__) 19 | 20 | 21 | def _make_translation(language): 22 | return str(T("partly cloudy", lang=language)) 23 | 24 | 25 | def test_translation(app): 26 | current.language = "en" 27 | assert _make_translation("it") == "nuvolosità variabile" 28 | assert _make_translation("de") == "teilweise bewölkt" 29 | assert _make_translation("ru") == "переменная облачность" 30 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.utils 4 | ----------- 5 | 6 | Test Emmett utils engine 7 | """ 8 | 9 | from emmett.datastructures import sdict 10 | from emmett.utils import dict_to_sdict, is_valid_ip_address 11 | 12 | 13 | def test_is_valid_ip_address(): 14 | result_localhost = is_valid_ip_address("127.0.0.1") 15 | assert result_localhost is True 16 | 17 | result_unknown = is_valid_ip_address("unknown") 18 | assert result_unknown is False 19 | 20 | result_ipv4_valid = is_valid_ip_address("::ffff:192.168.0.1") 21 | assert result_ipv4_valid is True 22 | 23 | result_ipv4_valid = is_valid_ip_address("192.168.256.1") 24 | assert result_ipv4_valid is False 25 | 26 | result_ipv6_valid = is_valid_ip_address("fd40:363d:ee85::") 27 | assert result_ipv6_valid is True 28 | 29 | result_ipv6_valid = is_valid_ip_address("fd40:363d:ee85::1::") 30 | assert result_ipv6_valid is False 31 | 32 | 33 | def test_dict_to_sdict(): 34 | result_sdict = dict_to_sdict({"test": "dict"}) 35 | assert isinstance(result_sdict, sdict) 36 | assert result_sdict.test == "dict" 37 | 38 | result_number = dict_to_sdict(1) 39 | assert not isinstance(result_number, sdict) 40 | assert result_number == 1 41 | -------------------------------------------------------------------------------- /tests/test_wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tests.wrappers 4 | -------------- 5 | 6 | Test Emmett wrappers module 7 | """ 8 | 9 | from emmett_core.protocols.rsgi.test_client.scope import ScopeBuilder 10 | from helpers import current_ctx 11 | 12 | from emmett.rsgi.wrappers import Request, Response 13 | 14 | 15 | def test_request(): 16 | scope, _ = ScopeBuilder( 17 | path="/?foo=bar", 18 | method="GET", 19 | ).get_data() 20 | request = Request(scope, None, None) 21 | 22 | assert request.query_params == {"foo": "bar"} 23 | assert request.client == "127.0.0.1" 24 | 25 | 26 | def test_response(): 27 | response = Response(None) 28 | 29 | assert response.status == 200 30 | assert response.headers["content-type"] == "text/plain" 31 | 32 | 33 | def test_req_ctx(): 34 | with current_ctx("/?foo=bar") as ctx: 35 | assert isinstance(ctx.request, Request) 36 | assert isinstance(ctx.response, Response) 37 | --------------------------------------------------------------------------------