├── .circleci
└── config.yml
├── .gitbook.yaml
├── .github
├── CODE_OF_CONDUCT.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── build.py
├── docs
├── client
│ ├── examples.md
│ ├── initial.md
│ ├── session.md
│ └── websocket.md
├── components
│ ├── initial.md
│ └── request.md
├── configs.md
├── contributing.md
├── deploy.md
├── events.md
├── extensions.md
├── faq.md
├── logging.md
├── logo.png
├── redirects
│ └── page.md
├── responses.md
├── routing.md
├── schemas
│ ├── fields.md
│ └── initial.md
├── started.md
├── summary.md
├── templates
│ ├── engine.md
│ ├── extending.md
│ ├── performance.md
│ └── syntax.md
└── testing
│ ├── advanced.md
│ └── started.md
├── requirements.txt
├── samples
├── benchmarks
│ ├── __init__.py
│ ├── client.py
│ └── template_engine.py
├── blueprints
│ ├── __init__.py
│ ├── run.py
│ ├── v1
│ │ ├── __init__.py
│ │ └── routes.py
│ └── v2
│ │ ├── __init__.py
│ │ └── routes.py
├── component.py
├── form.py
├── hooks.py
├── schemas.py
├── sessions
│ ├── __init__.py
│ ├── encrypted_cookies.py
│ ├── files.py
│ └── redis.py
├── simple.py
├── simple_json.py
├── static.py
├── static
│ └── app.js
├── streaming.py
├── templates.py
├── templates
│ ├── base.html
│ ├── header.html
│ └── index.html
├── templates_cython.py
├── upload.py
└── websockets.py
├── setup.cfg
├── setup.py
├── test.py
├── tests
├── __init__.py
├── blueprints.py
├── cache.py
├── client
│ ├── __init__.py
│ ├── external.py
│ ├── interface.py
│ ├── keep_alive.py
│ ├── multipart.py
│ ├── ssl_connections.py
│ └── streaming.py
├── components.py
├── exceptions.py
├── forms.py
├── headers.py
├── helpers.py
├── hooks.py
├── limits.py
├── responses.py
├── router
│ ├── __init__.py
│ ├── prefixes.py
│ └── strategies.py
├── schemas
│ ├── __init__.py
│ └── schemas.py
├── streaming.py
├── subdomains.py
├── templates
│ ├── __init__.py
│ ├── exceptions.py
│ ├── extensions.py
│ ├── nodes.py
│ └── render.py
└── timeouts.py
├── vendor
└── http-parser-2.8.1
│ ├── .gitignore
│ ├── .mailmap
│ ├── .travis.yml
│ ├── AUTHORS
│ ├── LICENSE-MIT
│ ├── Makefile
│ ├── README.md
│ ├── bench.c
│ ├── http_parser.c
│ ├── http_parser.gyp
│ ├── http_parser.h
│ └── test.c
└── vibora
├── __init__.py
├── __version__.py
├── application.py
├── blueprints.py
├── cache
├── __init__.py
├── cache.pxd
└── cache.py
├── client
├── __init__.py
├── connection.py
├── decoders.py
├── defaults.py
├── exceptions.py
├── limits.py
├── pool.py
├── request.py
├── response.py
├── retries.py
├── session.py
└── websocket.py
├── components
├── __init__.py
├── components.pxd
├── components.pyx
└── context.py
├── constants.py
├── context.py
├── cookies.py
├── exceptions.py
├── headers
├── __init__.py
├── headers.pxd
└── headers.py
├── hooks.py
├── limits.py
├── multipart
├── __init__.py
├── containers.py
├── parser.pxd
└── parser.pyx
├── optimizer.py
├── parsers
├── __init__.py
├── cparser.pxd
├── errors.py
├── parser.pxd
├── parser.pyx
├── response.pxd
├── response.pyx
└── typing.py
├── protocol
├── __init__.py
├── cprotocol.pxd
├── cprotocol.pyx
├── cwebsocket.pxd
├── cwebsocket.pyx
└── definitions.py
├── request
├── __init__.py
├── hints.py
├── request.pxd
└── request.pyx
├── responses
├── __init__.py
├── hints.py
├── responses.pxd
└── responses.pyx
├── router
├── __init__.py
├── parser.py
├── router.pxd
└── router.py
├── schemas
├── __init__.py
├── exceptions.py
├── extensions
│ ├── __init__.py
│ ├── fields.pxd
│ ├── fields.pyx
│ ├── schemas.pyx
│ ├── validator.pxd
│ └── validator.pyx
├── messages.py
├── schemas.py
├── types.py
└── validators.py
├── server.py
├── sessions
├── __init__.py
├── base.py
├── client.py
└── files.py
├── static.py
├── templates
├── __init__.py
├── ast.py
├── cache.py
├── compilers
│ ├── __init__.py
│ ├── base.py
│ ├── cython.py
│ ├── helpers.py
│ └── python.py
├── engine.py
├── exceptions.py
├── extensions.py
├── loader.py
├── nodes.py
├── parser.py
├── template.py
└── utils.py
├── tests.py
├── utils.py
├── websockets
├── __init__.py
└── obj.py
└── workers
├── __init__.py
├── handler.py
├── necromancer.py
└── reaper.py
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Python CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-python/ for more details
4 | #
5 | version: 2
6 | workflows:
7 | version: 2
8 | test:
9 | jobs:
10 | - test-3.6
11 | - test-3.7
12 | jobs:
13 | test-3.6: &test-template
14 | docker:
15 | - image: circleci/python:3.6-jessie
16 |
17 | working_directory: ~/repo
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "requirements.txt" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run:
30 | name: Installing dependencies
31 | command: |
32 | python3 -m venv venv
33 | . venv/bin/activate
34 | pip install -r requirements.txt
35 |
36 | - save_cache:
37 | paths:
38 | - ./venv
39 | key: v1-dependencies-{{ checksum "requirements.txt" }}
40 |
41 | - run:
42 | name: Building C/Cython extensions
43 | command: |
44 | . venv/bin/activate
45 | python build.py
46 |
47 | - run:
48 | name: Running Tests
49 | command: |
50 | . venv/bin/activate
51 | python test.py
52 | test-3.7:
53 | <<: *test-template
54 | docker:
55 | - image: circleci/python:3.7-rc-node
--------------------------------------------------------------------------------
/.gitbook.yaml:
--------------------------------------------------------------------------------
1 | root: /docs
2 | structure:
3 | readme: started.md
4 | summary: summary.md
5 | redirects:
6 | previous/page: redirects/page.md
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at frank@frankvieira.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Additional context**
24 | Add any other context about the problem here.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Before submitting a pull request**, please make sure the following is done:
2 |
3 | 1. Fork the repository and create your branch from master.
4 |
5 | 2. Run the build.py and test.py scripts.
6 |
7 | 3. If you are fixing something, add tests that fail in the current version and pass in yours.
8 |
9 | 4. If you're developing a new feature open an issue first to discuss it. Some features may be useful to you but harmful to others be it because of performance, new dependencies or different perspectives.
10 |
11 | 5. One more time: Don't forget to add tests.
12 |
13 | 6. Ensure the entire test suite is passing and overall performance is not degraded.
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.so
3 | *.c
4 | !vendor/http-parser-2.8.1/*.c
5 | build/
6 | __pycache__
7 | __compiled__
8 | __cache__
9 | dist
10 | vibora.egg-info
11 | .venv
12 |
13 | # Elastic Beanstalk Files
14 | .elasticbeanstalk/*
15 | !.elasticbeanstalk/*.cfg.yml
16 | !.elasticbeanstalk/*.global.yml
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-present Frank Vieira
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | global-include *.txt *.rst *.pyx *.pxd *.c *.h
2 | recursive-include *
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > **Warning: This project is being completely re-written. If you're curious about the progress, reach me on Slack.**
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | -----------------------------------------------------------
16 | > [Vibora](https://vibora.io) is a **fast, asynchronous and elegant** Python 3.6+ http client/server framework. (Alpha stage)
17 |
18 | > Before you ask, Vibora means Viper in Portuguese :)
19 |
20 |
21 | Server Features
22 | ---------------
23 | * Performance (https://github.com/vibora-io/benchmarks).
24 | * Schemas Engine.
25 | * Nested Blueprints / Domain Based Routes / Components
26 | * Connection Reaper / Self-Healing Workers
27 | * Sessions Engine
28 | * Streaming
29 | * Websockets
30 | * Caching tools
31 | * Async Template Engine (hot-reloading, deep inheritance)
32 | * Complete flow customization
33 | * Static Files (Smart Cache, Range, LastModified, ETags)
34 | * Testing Framework
35 | * Type hints, type hints, type hints everywhere.
36 |
37 |
38 | Client Features
39 | ---------------
40 | * Streaming MultipartForms (Inspired by: https://github.com/requests/requests/issues/1584)
41 | * Rate Limiting / Retries mechanisms
42 | * Websockets
43 | * Keep-Alive & Connection Pooling
44 | * Sessions with cookies persistence
45 | * Basic/digest Authentication
46 | * Transparent Content Decoding
47 |
48 | Server Example
49 | --------------
50 | ```python
51 | from vibora import Vibora, Request
52 | from vibora.responses import JsonResponse
53 |
54 | app = Vibora()
55 |
56 |
57 | @app.route('/')
58 | async def home(request: Request):
59 | return JsonResponse({'hello': 'world'})
60 |
61 |
62 | if __name__ == '__main__':
63 | app.run(debug=True, host='0.0.0.0', port=8000)
64 | ```
65 |
66 | Client Example
67 | --------------
68 |
69 | ```python
70 | import asyncio
71 | from vibora import client
72 |
73 |
74 | async def hello_world():
75 | response = await client.get('https://google.com/')
76 | print(f'Content: {response.content}')
77 | print(f'Status code: {response.status_code}')
78 |
79 |
80 | if __name__ == '__main__':
81 | loop = asyncio.get_event_loop()
82 | loop.run_until_complete(hello_world())
83 | ```
84 |
85 | Documentation
86 | -------------
87 | [Check it out at Vibora docs website](https://docs.vibora.io).
88 |
89 | Performance (Infamous Hello World benchmark)
90 | -----------
91 | | Frameworks | Requests/Sec | Version |
92 | | ------------- |:-------------:|:--------:|
93 | | Tornado | 14,197 | 5.0.2 |
94 | | Django | 22,823 | 2.0.6 |
95 | | Flask | 37,487 | 1.0.2 |
96 | | Aiohttp | 61,252 | 3.3.2 |
97 | | Sanic | 119,764 | 0.7.0 |
98 | | Vibora | 368,456 | 0.0.6 |
99 | > More benchmarks and info at https://github.com/vibora-io/benchmarks
100 | ----
101 | Goals
102 | -----
103 | * **Be the fastest Python http client/server framework.**.
104 | * Windows / Linux / MacOS.
105 | * Enjoyable and up to date development features/trends.
106 |
107 | Coming Soon
108 | -----------
109 | * Auto Reloading
110 | * HTTP2 Support
111 | * Brotli support (Server/Client)
112 | * Cython compiled templates.
113 | * Cython compiled user-routes.
114 |
--------------------------------------------------------------------------------
/build.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import subprocess
4 |
5 | project_path = os.path.join(os.path.dirname(__file__), 'vibora')
6 |
7 | # Seeking for files pending compilation.
8 | pending_compilation = set()
9 | for root, dirs, files in os.walk(project_path):
10 | for file in files:
11 | if file.endswith('.pyx'):
12 | pending_compilation.add(os.path.join(root, file))
13 | elif file.endswith('.py'):
14 | if (file[:-3] + '.pxd') in files:
15 | pending_compilation.add(os.path.join(root, file))
16 | elif file.endswith(('.so', '.c', '.html')):
17 | os.remove(os.path.join(root, file))
18 |
19 | # Calling Cython to compile our extensions.
20 | cython = os.path.join(os.path.dirname(sys.executable), 'cython')
21 | process = subprocess.run([cython] + list(pending_compilation) + ['--fast-fail'])
22 | if process.returncode != 0:
23 | raise SystemExit(f'Failed to compile .pyx files to C.')
24 |
25 | # Building native extensions.
26 | process = subprocess.run(
27 | [
28 | sys.executable,
29 | os.path.join(os.path.dirname(project_path), 'setup.py'),
30 | 'build_ext',
31 | '--inplace'
32 | ]
33 | )
34 | if process.returncode != 0:
35 | raise SystemExit(f'Failed to build native modules.')
36 |
37 | print('Build process completed successfully.')
38 |
--------------------------------------------------------------------------------
/docs/client/examples.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/docs/client/examples.md
--------------------------------------------------------------------------------
/docs/client/initial.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/docs/client/initial.md
--------------------------------------------------------------------------------
/docs/client/session.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/docs/client/session.md
--------------------------------------------------------------------------------
/docs/client/websocket.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/docs/client/websocket.md
--------------------------------------------------------------------------------
/docs/components/initial.md:
--------------------------------------------------------------------------------
1 | ### Components
2 |
3 | Every app has some hot objects that should be available almost
4 | everywhere. Maybe they are database instances, maybe request objects.
5 | Vibora call these objects `components`
6 |
7 | For now you should pay close attention to the `Request` component:
8 |
9 | This is the most important component and will be everywhere in your app.
10 | It holds all information related to the current request and
11 | also some useful references like the current application and route.
12 |
13 | You can ask for components in your route by using type hints:
14 |
15 | ```py
16 | from vibora import Vibora, Request, Response
17 |
18 | app = Vibora()
19 |
20 | @app.route('/')
21 | async def home(request: Request):
22 | print(request.headers)
23 | return Response(b'123')
24 | ```
25 |
26 | The request object has a special method that allows you to ask
27 | for more components as you go.
28 |
29 | ```py
30 | from vibora import Vibora, Request, Response
31 | from vibora import Route
32 |
33 | app = Vibora()
34 |
35 | @app.route('/')
36 | async def home(request: Request):
37 | current_route = request.get_component(Route)
38 | return Response(current_route.pattern.encode())
39 | ```
40 |
41 | > By now you should have noticed that Vibora is smart enough
42 | to know which components do you want in your routes so your routes
43 | may not receive any parameters at all or ask as many components
44 | do you wish.
45 |
46 |
47 | ### Adding custom components
48 |
49 | Vibora was designed to avoid global magic (unlike Flask for example)
50 | because it makes testing harder
51 | and more prone to errors specially in async environments.
52 |
53 | To help with this,
54 | Vibora provides an API where you can register objects to later use.
55 |
56 | This means they are correctly encapsulated into a single app object,
57 | allowing many apps instances to work concurrently,
58 | encouraging the use of type hints which brings many benefits
59 | in the long-term and also make your routes much easier to test.
60 |
61 | ```py
62 | from vibora import Vibora, Request, Response
63 | from vibora import Route
64 |
65 | # Config will be a new component.
66 | class Config:
67 | def __init__(self):
68 | self.name = 'Vibora Component'
69 |
70 | app = Vibora()
71 |
72 | # Registering the config instance.
73 | app.add_component(Config())
74 |
75 | @app.route('/')
76 | async def home(request: Request, config: Config):
77 | """
78 | Notice that if you specify a parameter of type "Config"
79 | Vibora will automatically provide the config instance registered previously.
80 | Instead of adding global variables you can now register new components,
81 | that are easily testable and accessible.
82 | """
83 | # You could also ask for the Config component at runtime.
84 | current_config = request.get_component(Config)
85 | assert current_config is config
86 | return Response(config.name)
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/components/request.md:
--------------------------------------------------------------------------------
1 | ### Request Component
2 |
3 | The request component holds all the information related
4 | to the current request.
5 | Json, Forms, Files everything can be accessed through it.
6 |
7 | ### Receiving JSON
8 |
9 | ```py
10 | from vibora import Vibora, Request
11 | from vibora.responses import JsonResponse
12 |
13 | app = Vibora()
14 |
15 | @app.route('/')
16 | async def home(request: Request):
17 | values = await request.json()
18 | print(values)
19 | return JsonResponse(values)
20 |
21 | app.run()
22 | ```
23 |
24 | Note that `request.json()` is actually a coroutine
25 | that needs to be **awaited**, this design prevents the entire JSON being
26 | uploaded in-memory before the route requires it.
27 |
28 |
29 | ### Uploaded Files
30 |
31 | Uploaded files by multipart forms can be accessed by
32 | field name in `request.form` or through the
33 | `request.files` list. Both methods are co-routines that will consume the
34 | `request.stream` and store the file in-disk if it's too big
35 | to keep in-memory.
36 |
37 | You can control the memory/disk usage of uploaded files by calling
38 | `request.load_form(threshold=1 * 1024 * 1024)` explicitly,
39 | in this case files bigger than 1mb will be flushed to disk.
40 |
41 | > Please be aware that the form threshold does not passthrough the
42 | max_body_size limit so you'll still need to configure your route
43 | properly.
44 |
45 | Instead of pre-parsing the entire form you could call
46 | `request.stream_form()` and deal with every uploaded field as
47 | it arrives by the network. This is good when you don't want files
48 | hitting the disk and in some scenarios allows you to waste less memory
49 | by doing way more coding yourself.
50 |
51 | ```py
52 | import uuid
53 | from vibora import Vibora, Request
54 | from vibora.responses import JsonResponse
55 |
56 | app = Vibora()
57 |
58 | @app.route('/', methods=['POST'])
59 | async def home(request: Request):
60 | uploaded_files = []
61 | for file in (await request.files):
62 | file.save('/tmp/' + str(uuid.uuid4()))
63 | print(f'Received uploaded file: {file.filename}')
64 | uploaded_files.append(file.filename)
65 | return JsonResponse(uploaded_files)
66 | ```
67 |
68 | ### Querystring
69 |
70 | ```py
71 | from vibora import Vibora, Response, Request
72 |
73 | app = Vibora()
74 |
75 | @app.route('/')
76 | async def home(request: Request):
77 | print(request.args)
78 | return Response(f'Name: {request.args['name']}'.encode())
79 | ```
80 | > A request to http://{address}/?name=vibora would return 'Name: vibora'
81 |
82 | ### Raw Stream
83 |
84 | Sometimes you need a low-level access to the HTTP request body,
85 | `request.stream` method provides an easy way to consume the
86 | stream by ourself.
87 |
88 | ```py
89 | from vibora import Vibora, Request, Response
90 |
91 | app = Vibora()
92 |
93 | @app.route('/', methods=['POST'])
94 | async def home(request: Request):
95 | content = await request.stream.read()
96 | return Response(content)
97 | ```
98 |
99 | ### URLs
100 |
101 | Ideally you shouldn't need to deal with the URL directly but
102 | sometimes that's the only way. The request object carries two properties
103 | that can help you:
104 |
105 | `request.url`: Raw URL
106 |
107 | `request.parsed_url`: A parsed URL where you can access the path,
108 | host and all URL attributes easily.
109 | The URL is parsed by a
110 | fast Cython parser so there is no need to you re-invent the wheel.
111 |
112 | ```py
113 | from vibora import Vibora, Request
114 | from vibora.responses import JsonResponse
115 |
116 | app = Vibora()
117 |
118 | @app.route('/')
119 | async def home(request: Request):
120 | return JsonResponse(
121 | {'url': request.url, 'parsed_url': request.parsed_url}
122 | )
123 | ```
124 |
--------------------------------------------------------------------------------
/docs/configs.md:
--------------------------------------------------------------------------------
1 | ### Configuration
2 |
3 | Configuration handling in Vibora is simple thanks to components.
4 |
5 | In your init script (usually called run.py) you can load environment
6 | variables, config files or whatever and register a
7 | config class as a new component and that's all.
8 |
9 | This method is a little bit harder for beginners when compared to the
10 | Django approach but it's way more flexible and allows you to build
11 | whatever suits you better.
12 |
13 | Here goes a practical example:
14 |
15 | 1) Create a file called config.py
16 | ```py
17 | import aioredis
18 |
19 |
20 | class Config:
21 | def __init__(self, config: dict):
22 | self.port = config['port']
23 | self.host = config['host']
24 | self.redis_host = config['redis']['host']
25 | ```
26 |
27 | 2) Create a file called api.py
28 | ```py
29 | from vibora import Vibora
30 | from vibora.blueprints import Blueprint
31 | from vibora.hooks import Events
32 | from aioredis import ConnectionsPool
33 | from config import Config
34 |
35 | api = Blueprint()
36 |
37 |
38 | @api.route('/')
39 | async def home(pool: ConnectionsPool):
40 | await pool.set('my_key', 'any_value')
41 | value = await pool.get('my_key')
42 | return Response(value.encode())
43 |
44 |
45 | @api.handle(Events.BEFORE_SERVER_START)
46 | async def initialize_db(app: Vibora, config: Config):
47 |
48 | # Creating a pool of connection to Redis.
49 | pool = await aioredis.create_pool(config.redis_host)
50 |
51 | # In this case we are registering the pool as a new component
52 | # but if you find yourself using too many components
53 | # feel free to wrap them all inside a single component
54 | # so you don't need to repeat yourself in every route.
55 | app.components.add(pool)
56 | ```
57 |
58 | 3) Now create a file called config.json
59 | ```js
60 | {
61 | "host": "0.0.0.0",
62 | "port": 8000,
63 | "redis_host": "127.0.0.1"
64 | }
65 | ```
66 |
67 | 4) Now create a file called run.py
68 |
69 | ```py
70 | import json
71 | from vibora import Vibora
72 | from api import api
73 | from config import Config
74 |
75 |
76 | if __name__ == "__main__":
77 | # Creating a new app
78 | app = Vibora()
79 |
80 | # Registering our API
81 | app.add_blueprint(api, prefixes={'v1': '/v1'})
82 |
83 | # Opening the configuration file.
84 | with open('config.json') as f:
85 |
86 | # Parsing the JSON configs.
87 | config = Config(json.load(f))
88 |
89 | # Registering the config as a component so you can use it
90 | # later on (as we do in the "before_server_start" hook)
91 | app.components.add(config)
92 |
93 | # Running the server.
94 | app.run(host=config.host, port=config.port)
95 | ```
96 |
97 | The previous example loads your configuration from JSON files, but
98 | other approaches, such as environment variables, can be used.
99 |
100 | Notice that we register the config instance as a component because
101 | databases drivers, for example, often need to be instantiated
102 | after the server is forked so you'll need the config after
103 | the "run script".
104 |
105 | Also, our config class in this example is a mere wrapper for our JSON
106 | config but in a real app, you could be using the config class as
107 | a components wrapper. You'll just need to add references to many
108 | important components so you don't need to repeat yourself by
109 | importing many different components in every route.
110 |
--------------------------------------------------------------------------------
/docs/contributing.md:
--------------------------------------------------------------------------------
1 | ### Contributing
2 |
3 | Vibora is developed on GitHub and pull requests are welcome but there
4 | are few guidelines:
5 |
6 | 1) Introduction of new external dependencies is highly discouraged
7 | and will probably not be merged.
8 |
9 | 2) Patches that downgrade the overall framework performance, unless
10 | security/fix ones, will need to prove great value in functionality
11 | to be merged.
12 |
13 | 3) Bug fixes must include tests that fail/pass in respective versions.
14 |
15 | 4) PEP 8 must be followed with the exception of the
16 | max line size which is currently 120 instead of 80 chars wide.
17 |
18 | ### Reporting an issue
19 |
20 | 1) Describe what you expected to happen and what actually happens.
21 |
22 | 2) If possible, include a minimal but complete example
23 | to help us reproduce the issue.
24 |
25 | 3) We'll try to fix it as soon as possible but be in mind that
26 | Vibora is open source and you can probably submit a pull request
27 | to fix it even faster.
28 |
29 | ### First time setup
30 |
31 | 1) Clone Vibora repository.
32 |
33 | 2) Create a virtualenv and install the dependencies listed on
34 | requirements.txt
35 |
36 | 3) Run build.py (Vibora has a lot of cython extensions and this file
37 | helps to build them so you can test your code without the need to
38 | install or compile libraries manually.
--------------------------------------------------------------------------------
/docs/deploy.md:
--------------------------------------------------------------------------------
1 | ### Deployment
2 |
3 | Vibora is not a WSGI compatible framework because of its async nature.
4 | Its own http server is built to battle so deployment is far easier
5 | than with other frameworks because there is no need for Gunicorn/uWSGI.
6 |
7 | One may argue that Gunicorn/uWSGI are battle proven solutions and that's true
8 | but they also bring different applications behaviors between dev/prod
9 | environments and still need a battle tested server as Nginx
10 | in front of them.
11 |
12 | The recommend approach to freeze a Vibora app is using docker,
13 | this way you can build a frozen image locally in your machine, test it
14 | and upload to wherever you host. This way you skip
15 | all python packaging problems that you'll find trying to build
16 | reproducible deployments between different machines.
17 |
--------------------------------------------------------------------------------
/docs/events.md:
--------------------------------------------------------------------------------
1 | ### Hooks (Listeners)
2 |
3 | Hooks are functions that are called after an event.
4 |
5 | Let's suppose you want to add a header to every response in your app.
6 | Instead of manually editing every single route in your app you can just
7 | register a listener to the event "BeforeResponse" and inject the desired headers.
8 |
9 | Below is a fully working example:
10 |
11 | ```py
12 | from vibora import Vibora, Response
13 | from vibora.hooks import Events
14 |
15 | app = Vibora()
16 |
17 | @app.route('/')
18 | async def home():
19 | return Response(b'Hello World')
20 |
21 | @app.handle(Events.BEFORE_RESPONSE)
22 | async def before_response(response: Response):
23 | response.headers['x-my-custom-header'] = 'Hello :)'
24 |
25 | if __name__ == '__main__':
26 | app.run()
27 | ```
28 |
29 | Hooks can halt a request and prevent a route from being called,
30 | completely modify the response, handle app start/stop functionalities,
31 | initialize components and do all kind of stuff.
32 |
33 | > The golden rule is: If you don't want to modify the request flow (like halting requests)
34 | you don't want to return anything in your function.
35 | Of course that depends on which event you are listening to.
36 |
--------------------------------------------------------------------------------
/docs/extensions.md:
--------------------------------------------------------------------------------
1 | Under construction
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ### Frequently Asked Questions
2 |
3 | #### Why Vibora ?
4 |
5 | - I needed a framework like Flask but async by design.
6 |
7 | - Sanic is a good idea with questionable design choices (IMHO).
8 |
9 | - Aiohttp is solid (and well thought) but I dislike some
10 | interfaces and I think many of them could be user-friendlier.
11 |
12 | - I was unaware of Quart and I have mixed feelings about
13 | being __compatible__ with Flask.
14 |
15 | - Japronto is currently a proof of concept, a very impressive one.
16 |
17 | - Apistar, although I like it, is far away from being like Flask.
18 |
19 | - I don't like Tornado APIs, they did an awesome job don't get me wrong.
20 |
21 | - Big Upload/Downloads is a pain the ass in most frameworks thanks to WSGI.
22 |
23 | - Flask/Django are sync and always will.
24 | Don't get me wrong, being sync isn't bad but it just doesn't fit in some situations.
25 | You can do whatever magic you want to make them async but sync interfaces like "request.json" will haunt you.
26 |
27 | - I'm a big fan of type hints and very few projects use them.
28 |
29 | - And finally because history always repeats itself and here we are, again, with another framework.
30 |
31 | #### Where the performance comes from ?
32 |
33 | - Cython. Critical framework pieces are written Cython so it can leverage "C speed" in critical stuff.
34 |
35 | - Common tasks as schema validation, template rendering and other stuff were made builtin in the framework,
36 | written from scratch with performance in mind.
37 |
38 | #### Is it compatible with PyPy ?
39 |
40 | - No. PyPy's poor C extensions compatibility (performance-wise) is it's biggest problem.
41 | Vibora would need to drop its C extensions or have duplicate implementations (Cython powered X pure Python).
42 | In the end I would bet that Vibora on PyPy would still be slower than the Cython-powered version.
43 | I'm open to suggestions and I'm watching PyPy closely so who knows.
44 |
45 | #### Why not use Jinja2 ?
46 |
47 | - Jinja2 was not built with async in mind.
48 |
49 | - I would need to write a cython compiler for it anyways (Vibora one is in-progress).
50 |
51 | - I want a bit more freedom in the template syntax.
52 |
53 | - And of course: because it looked like an exciting challenge.
54 |
55 | #### Where is Japronto on benchmarks ?
56 |
57 | - Vibora was almost twice as fast before network flow control was a concern,
58 | what that means is that it is very easy to write a fast server but not so easy to build a stable one.
59 |
60 | - Although Japronto inspired some pieces of this framework
61 | it's missing a huge chunk of fixes and features.
62 |
63 | - The author of the framework does not encourage the usage of it and so do I.
64 |
65 | - Japronto may be faster than Vibora on naked benchmarks thanks to impressive hand-coded C
66 | and faster HTTP parser (pico X noyent).
67 |
68 | - Vibora may use "picohttparser" in the future but right now I don't think it's a wise move because
69 | it's less battle tested.
70 |
71 | - Hand-coded C extensions can be a nightmarish hell to non-expert C devs so I'm not
72 | willing to replace Cython with baby cared C code. Still I'm willing to replace Cython with Rust extensions
73 | if they get stable enough.
74 |
75 | #### Why don't you export the template engine into a new project ?
76 |
77 | - If people show interest, why not.
78 |
79 | #### What about Trio ?
80 |
81 | - Trio has some interesting concepts and although it's better
82 | than asyncio in overall I'm not sure about it. The python async community
83 | is still young and splitting it is not good. We already have a bunch
84 | of libraries and uvloop so it's hard to move now. I would like to see
85 | some of it's concepts implemented on top of asyncio but that needs some
86 | serious creativity because of asyncio design.
87 |
88 | #### Can we make Vibora faster ?
89 |
90 | - Sure. I have a bunch of ideas but I'm a one man army. Are you willing to help me ? :)
91 |
--------------------------------------------------------------------------------
/docs/logging.md:
--------------------------------------------------------------------------------
1 | ### Logging
2 |
3 | Vibora has a simple logging mechanism to avoid locking you into our library of choice.
4 |
5 | You must provide a function that receives two parameters: a msg and a logging level (that matches logging standard library for usability sake).
6 |
7 | That's all.
8 |
9 | It's up to you to choose what to do with logging messages.
10 |
11 |
12 | ```py
13 | import logging
14 | from vibora import Vibora, Response
15 |
16 | app = Vibora()
17 |
18 | @app.route('/')
19 | def home():
20 | return Response(b'Hello World')
21 |
22 | if __name__ == '__main__':
23 | def log_handler(msg, level):
24 | # Redirecting the msg and level to logging library.
25 | getattr(logging, level)(msg)
26 | print(f'Msg: {msg} / Level: {level}')
27 |
28 | app.run(logging=log_handler)
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/docs/logo.png
--------------------------------------------------------------------------------
/docs/redirects/page.md:
--------------------------------------------------------------------------------
1 | redirect
--------------------------------------------------------------------------------
/docs/responses.md:
--------------------------------------------------------------------------------
1 | ### Responses
2 |
3 | Each route must return a Response object,
4 | the protocol will use these objects to encode the HTTP response and
5 | send through the socket.
6 |
7 | There are many different response types but they all inherit from
8 | the base Response class.
9 |
10 | Bellow there are the most important ones:
11 |
12 | ### JSON Response
13 |
14 | Automatically dumps Python objects and adds the correct headers to match the JSON format.
15 |
16 | ```py
17 | from vibora import Vibora, JsonResponse
18 |
19 | app = Vibora()
20 |
21 | @app.route('/')
22 | async def home():
23 | return JsonResponse({'hello': 'world'})
24 | ```
25 |
26 | ### Streaming Response
27 |
28 | Whenever you don't have the response already completely ready,
29 | be it because you don't want to waste memory by buffering,
30 | be it because you want the client to start receiving the response as soon as possible,
31 | a StreamingResponse will be more appropriate.
32 |
33 | **A StreamingResponse receives a coroutine that yield bytes.**
34 |
35 | Differently from simple responses, streaming ones have more timeout options
36 | because they are often long running tasks.
37 | Usually a route timeout works until the client consumes the entire response
38 | but with streaming responses this is not true.
39 | After the route return a StreamingResponse two new timeouts options take its place.
40 |
41 | > **complete_timeout: int**: How many seconds the client have to consume the **entire** response.
42 | So if you set it to 30 seconds the client will have 30 seconds to consume the entire response,
43 | in case not, the connection will be closed abruptly to avoid DOS attacks.
44 | You may set it to zero and completely disable this timeout,
45 | when chunk_timeout is properly configured this is a reasonable choice.
46 |
47 | > **chunk_timeout: int**: How many seconds the client have to consume each response chunk.
48 | Lets say your function produces 30 bytes per yield and the chunk_timeout is 10 seconds.
49 | The client will have 10 seconds to consume the 30 bytes, in case not, the connection will be closed abruptly to avoid DOS attacks.
50 |
51 | ```py
52 | import asyncio
53 | from vibora import Vibora, StreamingResponse
54 |
55 | app = Vibora()
56 |
57 | @app.route('/')
58 | async def home():
59 | async def stream_builder():
60 | for x in range(0, 5):
61 | yield str(x).encode()
62 | await asyncio.sleep(1)
63 |
64 | return StreamingResponse(
65 | stream_builder, chunk_timeout=10, complete_timeout=30
66 | )
67 | ```
68 |
69 | ### Response
70 |
71 | A raw Response object would fit whenever you need a more
72 | customized response.
73 |
74 | ```py
75 | from vibora import Vibora, Response
76 |
77 | app = Vibora()
78 |
79 | @app.route('/')
80 | async def home():
81 | return Response(b'Hello World', headers={'content-type': 'html'})
82 | ```
83 |
--------------------------------------------------------------------------------
/docs/schemas/fields.md:
--------------------------------------------------------------------------------
1 | ### Fields
2 |
3 | Vibora has a special class called "Field" to represent each field
4 | of a schema. You can build any kind of validation rules using this class
5 | but to avoid repeat yourself there a few builtin ones.
6 | There are a few must-know attributes of this class:
7 |
8 | 1) **required** -> By default all declared fields in a schema are required which means
9 | they must be present in the validation values.
10 | If you have optional fields you must explicitely declare this as
11 | `Field(required=False)`
12 |
13 | 2) **load_from** -> Sometimes is useful to deal with friendly names
14 | inside a schema but to ofuscate them outside outside your app, by using the `load_from` parameter
15 | you can specify where to load this field from or even load two
16 | different fields from the same key.
17 |
18 | 3) **default** -> A default value in case the key is missing
19 | or the value is null.
20 |
21 | 4) **validators** -> A list of functions to validate the current value against.
22 | This functions can be async or sync and receive one up to two parameters.
23 | In case it receives a single parameter then Vibora will pass only the current value to it.
24 | In case it receive two parameters the context of the schema will be also provided.
25 | The exception `ValidationError` must be raised to notify the schema that this field is invalid,
26 | returning values are ignored.
27 |
28 | ### StringField
29 |
30 | Validates if the given value is a valid string.
31 |
32 | ```py
33 | import uuid
34 | from vibora.schemas import Schema, fields
35 | from vibora.schemas.validators import Length
36 |
37 | class NewUserSchema(Schema):
38 |
39 | name: str = fields.String(
40 | required=False,
41 | validators=[Length(min=3, max=30)],
42 | default=lambda: str(uuid.uuid4()),
43 | strict=False
44 | )
45 | ```
46 |
47 | > There is a special attribute called `strict` to allow this field to cast
48 | integers and similar types to a string instead of raising an error.
--------------------------------------------------------------------------------
/docs/schemas/initial.md:
--------------------------------------------------------------------------------
1 | ### Data Validation
2 |
3 | Data validation is a common task in any web related activity.
4 | Vibora has a module called `schemas` to build, guess what,
5 | schemas, and validate your data against them.
6 | They are very similar to `marshmallow` and
7 | other famous libraries except they have some speedups written in Cython
8 | for amazing performance.
9 |
10 | Schemas are also asynchronous meaning that you can do
11 | database checkups and everything in a single place,
12 | something that cannot be done in other libraries which forces you to
13 | split your validation logic between different places.
14 |
15 | ### Usage Example
16 |
17 | ###### Declaring your schema
18 | ```py
19 | from vibora.schemas import Schema, fields
20 | from vibora.schemas.exceptions import ValidationError
21 | from vibora.schemas.validators import Length, Email
22 | from vibora.context import get_component
23 | from .database import Database
24 |
25 |
26 | class AddUserSchema(Schema):
27 |
28 | @staticmethod
29 | async def unique_email(email: str):
30 | # You can get any existent component by using "vibora.context"
31 | database = get_component(Database)
32 | if await database.exists_user(email):
33 | raise ValidationError(
34 | 'There is already a registered user with this e-mail'
35 | )
36 |
37 | # Custom validations can be done by passing a list of functions
38 | # to the validators keyword param.
39 | email: str = fields.Email(pattern='.*@vibora.io',
40 | validators=[unique_email]
41 | )
42 |
43 | # There are many builtin validation helpers as Length().
44 | password: str = fields.String(validators=[Length(min=6, max=20)])
45 |
46 | # In case you just want to enforce the type of a given field,
47 | # a type hint is enough.
48 | name: str
49 | ```
50 |
51 | ###### Using your schema
52 |
53 | ```py
54 | from vibora import Request, Blueprint, JsonResponse
55 | from .schemas import AddUserSchema
56 | from .database import Database
57 |
58 | users_api = Blueprint()
59 |
60 | @users_api.route('/add')
61 | async def add_user(request: Request, database: Database):
62 |
63 | # In case the schema is invalid an exception will be raised
64 | # and catched by an exception handler, this means you don't need to
65 | # repeat yourself about handling errors. But in case you want to
66 | # customize the error message feel free to catch the exception
67 | # and handle it your way. "from_request" method is just syntatic sugar
68 | # to avoid calling request.json() yourself.
69 | schema = await AddUserSchema.from_request(request)
70 |
71 | # By now our data is already valid and clean,
72 | # so lets add our user to the database.
73 | database.add_user(schema)
74 |
75 | return JsonResponse({'msg': 'User added successfully'})
76 | ```
77 |
78 | > Type hints must always be provided for each field. In case the field is always
79 | required and do not have any custom validation the type hint alone
80 | will be enough to Vibora build your schema.
81 |
--------------------------------------------------------------------------------
/docs/started.md:
--------------------------------------------------------------------------------
1 | ## Getting Started
2 |
3 | Make sure you are using `Python 3.6+` because Vibora takes
4 | advantage of some new Python features.
5 |
6 | 1. Install Vibora: `pip install vibora[fast]`
7 |
8 | > It's highly recommended to install Vibora inside a virtualenv.
9 |
10 | > In case you have trouble with Vibora dependencies: `pip install vibora` to install it without the extra libraries.
11 |
12 |
13 | 2. Create a file called `anything.py` with the following code:
14 |
15 |
16 | ```py
17 | from vibora import Vibora, JsonResponse
18 |
19 | app = Vibora()
20 |
21 |
22 | @app.route('/')
23 | async def home():
24 | return JsonResponse({'hello': 'world'})
25 |
26 | if __name__ == '__main__':
27 | app.run(host="0.0.0.0", port=8000)
28 | ```
29 |
30 | 3. Run the server: `python3 anything.py`
31 |
32 |
33 | 4. Open your browser at `http://127.0.0.1:8000`
34 |
35 |
36 | ### Creating a project
37 |
38 | The previous example was just to show off how easy is it
39 | to spin up a server.
40 |
41 | The recommended way to start a new project is by letting Vibora do it for you.
42 | Vibora is also a command-line tool, try it out: `vibora new project_name`
43 |
--------------------------------------------------------------------------------
/docs/summary.md:
--------------------------------------------------------------------------------
1 | * [Introduction](started.md)
2 | * [Routing](routing.md)
3 | * [Components](components/initial.md)
4 | * [Request Component](components/request.md)
5 | * [Responses](responses.md)
6 | * [Data Validation](schemas/initial.md)
7 | * [Fields](schemas/fields.md)
8 | * [Events](events.md)
9 | * [Testing](testing/started.md)
10 | * [Advanced Tips](testing/advanced.md)
11 | * [Template Engine](templates/engine.md)
12 | * [Syntax](templates/syntax.md)
13 | * [Extending](templates/extending.md)
14 | * [Performance](templates/performance.md)
15 | * [Logging](logging.md)
16 | * [Configuration](configs.md)
17 | * [Deployment](deploy.md)
18 | * [HTTP Client](client/initial.md)
19 | * [Session](client/session.md)
20 | * [Useful Examples](client/examples.md)
21 | * [Extensions](extensions.md)
22 | * [Contributing](contributing.md)
23 | * [FAQ](faq.md)
--------------------------------------------------------------------------------
/docs/templates/engine.md:
--------------------------------------------------------------------------------
1 | ### Vibora Template Engine (VTE)
2 |
3 | Although server-side rendering is not main-stream nowadays, Vibora has
4 | its own template engine. The idea was to build something like Jinja2
5 | but with async users as first class citizens. Jinja2 is already heavily
6 | optimized but we tried to beat it in benchmarks.
7 |
8 | Jinja2 also prevents you to pass parameters to functions and a few other
9 | restrictions which are often a good idea but don't comply with Vibora
10 | philosophy of not getting into your way.
11 |
12 | The syntax is pretty similar to Jinja2, templates are often compatible.
13 |
14 | The render process is async which means you can pass coroutines to your
15 | templates and call them as regular functions, Vibora will do the magic.
16 |
17 | VTE has hot-reloading so we can swap templates at run-time.
18 | This is enabled by default in debug mode so you have a fast iteration
19 | cycle while building your app.
20 |
21 | Although VTE **do not aim to be sandboxed** it tries hard to prevent the templates from leaking access to outside context.
22 |
--------------------------------------------------------------------------------
/docs/templates/extending.md:
--------------------------------------------------------------------------------
1 | WIP...
--------------------------------------------------------------------------------
/docs/templates/performance.md:
--------------------------------------------------------------------------------
1 | WIP...
--------------------------------------------------------------------------------
/docs/templates/syntax.md:
--------------------------------------------------------------------------------
1 | ### Template Syntax
2 |
3 | VTE syntax is basically split between two things: Tags and Expressions.
4 |
5 | ```html
6 |
7 |
8 | {{ title }}
9 |
10 |
11 |
12 | {% for user in users %}
13 | - {{ user.name}}
14 | {% endfor %}
15 |
16 |
17 |
18 | ```
19 |
20 | 1) Expressions are delimited by "{ { variable_name } }" and they are used to print data.
21 |
22 | 2) Tags are delimited by "{ % tag_name % }" and they are used to express intents like loops, conditionals, etc.
23 |
24 | > There are many default tags and you can create your own too
25 | by adding an extension, you can also customize the markers so instead of
26 | "{%" you could use "#[" or whatever do you think it's best.
27 |
--------------------------------------------------------------------------------
/docs/testing/advanced.md:
--------------------------------------------------------------------------------
1 | Under construction
--------------------------------------------------------------------------------
/docs/testing/started.md:
--------------------------------------------------------------------------------
1 | ### Testing
2 |
3 | Testing is the most important part of any project with considerably
4 | size and yet of one of the most ignored steps.
5 |
6 | Vibora has a builtin and fully featured async HTTP client and
7 | a simple test framework to make it easier for you as in the example bellow:
8 |
9 | ```py
10 | from vibora import Vibora, Response
11 | from vibora.tests import TestSuite
12 |
13 | app = Vibora()
14 |
15 |
16 | @app.route('/')
17 | async def home():
18 | return Response(b'Hello World')
19 |
20 |
21 | class HomeTestCase(TestSuite):
22 | def setUp(self):
23 | self.client = app.test_client()
24 |
25 | async def test_home(self):
26 | response = await self.client.get('/')
27 | self.assertEqual(response.content, b'Hello World')
28 | ```
29 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | cython==0.28.3
--------------------------------------------------------------------------------
/samples/benchmarks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/samples/benchmarks/__init__.py
--------------------------------------------------------------------------------
/samples/benchmarks/client.py:
--------------------------------------------------------------------------------
1 | import time
2 | import uvloop
3 | import requests
4 | from vibora.client import Session, RetryStrategy
5 | from vibora.client.limits import RequestRate
6 | from aiohttp import ClientSession
7 |
8 |
9 | max = 20
10 | url = 'http://127.0.0.1:8000/'
11 |
12 |
13 | async def vibora():
14 | t1 = time.time()
15 | with Session(keep_alive=True) as client:
16 | for _ in range(0, max):
17 | await client.get(url)
18 | print('Vibora: ', time.time() - t1)
19 |
20 |
21 | async def aiohttp():
22 | t1 = time.time()
23 | async with ClientSession() as session:
24 | for _ in range(0, max):
25 | await session.get(url)
26 | print('Aiohttp: ', time.time() - t1)
27 |
28 |
29 | async def requests2():
30 | t1 = time.time()
31 | with requests.Session() as session:
32 | for _ in range(0, max):
33 | session.get(url)
34 | print('Requests: ', time.time() - t1)
35 |
36 |
37 | if __name__ == '__main__':
38 | loop = uvloop.new_event_loop()
39 | loop.run_until_complete(vibora())
40 | loop.run_until_complete(aiohttp())
41 | loop.run_until_complete(requests2())
42 |
--------------------------------------------------------------------------------
/samples/benchmarks/template_engine.py:
--------------------------------------------------------------------------------
1 | import time
2 | import uvloop
3 | from inspect import isasyncgenfunction, isasyncgen, iscoroutinefunction
4 | from jinja2 import Template
5 | from vibora.templates import Template as VTemplate, TemplateEngine
6 |
7 |
8 | async def b():
9 | return 'oi'
10 |
11 |
12 | def gen():
13 | for x in range(0, 1000):
14 | yield x
15 |
16 |
17 | y = gen()
18 | rounds = 1000
19 | content = '{% for b in y%}{{x()}}{% endfor %}'
20 | # content = '{{ x() }}'
21 | engine = TemplateEngine()
22 | engine.add_template(VTemplate(content), names=['test'])
23 | engine.compile_templates(verbose=True)
24 |
25 |
26 | async def render():
27 | template = engine.loaded_templates['test']
28 | c = engine.prepared_calls[template]
29 | t1 = time.time()
30 | for _ in range(0, rounds):
31 | asd = ''
32 | async for x in c({'x': b, 'y': y}):
33 | asd += x
34 | print('Vibora: ', time.time() - t1)
35 |
36 | loop = uvloop.new_event_loop()
37 | loop.run_until_complete(render())
38 |
39 | t1 = time.time()
40 | t = Template(content, enable_async=True)
41 | for _ in range(0, rounds):
42 | t.render({'x': b, 'y': y})
43 | print('Jinja2: ', time.time() - t1)
44 |
45 |
--------------------------------------------------------------------------------
/samples/blueprints/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/samples/blueprints/__init__.py
--------------------------------------------------------------------------------
/samples/blueprints/run.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import JsonResponse
3 | from samples.blueprints.v1.routes import v1
4 | from samples.blueprints.v2.routes import v2
5 |
6 |
7 | app = Vibora()
8 |
9 |
10 | @app.route('/')
11 | def home():
12 | return JsonResponse({'a': 1})
13 |
14 |
15 | @app.handle(404)
16 | def handle_anything():
17 | app.url_for('')
18 | return JsonResponse({'global': 'handler'})
19 |
20 |
21 | if __name__ == '__main__':
22 | v1.add_blueprint(v2, prefixes={'v2': '/v2'})
23 | app.add_blueprint(v1, prefixes={'v1': '/v1', '': '/'})
24 | app.add_blueprint(v2, prefixes={'v2': '/v2'})
25 | app.run(debug=True, port=8000, host='0.0.0.0', workers=1)
26 |
--------------------------------------------------------------------------------
/samples/blueprints/v1/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/samples/blueprints/v1/__init__.py
--------------------------------------------------------------------------------
/samples/blueprints/v1/routes.py:
--------------------------------------------------------------------------------
1 | from vibora.responses import JsonResponse
2 | from vibora.blueprints import Blueprint
3 |
4 | v1 = Blueprint()
5 |
6 |
7 | @v1.route('/')
8 | def home():
9 | return JsonResponse({'hello': 'world'})
10 |
11 |
12 | @v1.route('/exception')
13 | def exception():
14 | raise Exception('oi')
15 |
16 |
17 | @v1.handle(IOError)
18 | def handle_exception():
19 | return JsonResponse({'msg': 'Exception caught correctly.'})
20 |
--------------------------------------------------------------------------------
/samples/blueprints/v2/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/samples/blueprints/v2/__init__.py
--------------------------------------------------------------------------------
/samples/blueprints/v2/routes.py:
--------------------------------------------------------------------------------
1 | from vibora.responses import JsonResponse
2 | from vibora.blueprints import Blueprint
3 |
4 | v2 = Blueprint()
5 |
6 |
7 | @v2.route('/')
8 | def home():
9 | return JsonResponse({'a': 2})
10 |
11 |
12 | @v2.route('/exception')
13 | def exception():
14 | raise IOError('oi')
15 |
16 |
17 | @v2.handle(Exception)
18 | def handle_exception():
19 | return JsonResponse({'msg': 'Exception catched correctly on v2.'})
20 |
--------------------------------------------------------------------------------
/samples/component.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.request import Request
3 | from vibora.responses import Response, JsonResponse
4 |
5 |
6 | class Config:
7 | def __init__(self):
8 | self.port = 123
9 |
10 |
11 | class Request2(Request):
12 | def __init__(self, c: Config, *args, **kwargs):
13 | super().__init__(*args, **kwargs)
14 | self.config = c
15 |
16 |
17 | app = Vibora()
18 |
19 |
20 | @app.route('/', cache=False)
21 | def home(request: Request2):
22 | return JsonResponse({'a': 1}, headers={'1': '1'})
23 |
24 |
25 | if __name__ == '__main__':
26 | config = Config()
27 |
28 | def create_request(*args, **kwargs) -> 'Request2':
29 | return Request2(config, *args, **kwargs)
30 | app.override_request(create_request)
31 | app.run(debug=False, port=8000, host='0.0.0.0')
32 |
--------------------------------------------------------------------------------
/samples/form.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.request import Request
3 | from vibora.responses import JsonResponse
4 | from vibora.static import StaticHandler
5 |
6 |
7 | app = Vibora(
8 | static=StaticHandler(['/tmp'], url_prefix='/static')
9 | )
10 |
11 |
12 | @app.route('/', methods=['POST'])
13 | async def home(request: Request):
14 | await request.form()
15 | return JsonResponse({'hello': 'world'})
16 |
17 |
18 | if __name__ == '__main__':
19 | app.run(debug=False, port=8000, host='0.0.0.0')
20 |
--------------------------------------------------------------------------------
/samples/hooks.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Events, Request
2 | from vibora.responses import JsonResponse
3 |
4 |
5 | app = Vibora()
6 |
7 |
8 | @app.route('/', cache=False)
9 | async def home():
10 | return JsonResponse({'1': 1})
11 |
12 |
13 | @app.handle(Events.BEFORE_ENDPOINT)
14 | async def before_endpoint(request: Request):
15 | request.context['1'] = 1
16 |
17 |
18 | @app.handle(Events.AFTER_ENDPOINT)
19 | async def after_endpoint():
20 | pass
21 |
22 |
23 | if __name__ == '__main__':
24 | app.run(debug=False, port=8888, host='localhost')
25 |
--------------------------------------------------------------------------------
/samples/schemas.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Request
2 | from vibora.responses import JsonResponse
3 | from vibora.schemas import Schema, fields
4 | from vibora.schemas.exceptions import InvalidSchema
5 |
6 | app = Vibora()
7 |
8 |
9 | class BenchmarkSchema(Schema):
10 | field1: str = fields.String(required=True)
11 | field2: int = fields.Integer(required=True)
12 |
13 |
14 | @app.route('/', methods=['POST'])
15 | async def home(request: Request):
16 | try:
17 | values = await BenchmarkSchema.load_json(request)
18 | return JsonResponse({'msg': 'Successfully validated', 'field1': values.field1,
19 | 'field2': values.field2})
20 | except InvalidSchema:
21 | return JsonResponse({'msg': 'Data is invalid'})
22 |
23 |
24 | if __name__ == '__main__':
25 | app.run(debug=True, port=8000, host='0.0.0.0', workers=8)
26 |
--------------------------------------------------------------------------------
/samples/sessions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/samples/sessions/__init__.py
--------------------------------------------------------------------------------
/samples/sessions/encrypted_cookies.py:
--------------------------------------------------------------------------------
1 | from cryptography.fernet import Fernet
2 | from vibora import Vibora, JsonResponse, Request
3 | from vibora.hooks import Events
4 | from vibora.sessions.client import EncryptedCookiesEngine
5 |
6 |
7 | app = Vibora()
8 |
9 |
10 | @app.handle(Events.BEFORE_SERVER_START)
11 | async def set_up_sessions(current_app: Vibora):
12 | # In this example every time the server is restart a new key will be generated, which means
13 | # you lose all your sessions... you may want to have a fixed key instead.
14 | current_app.session_engine = EncryptedCookiesEngine(
15 | 'cookie_name', secret_key=Fernet.generate_key()
16 | )
17 |
18 |
19 | @app.route('/')
20 | async def home(request: Request):
21 | session = await request.session()
22 | if 'requests_count' in session:
23 | session['requests_count'] += 1
24 | else:
25 | session['requests_count'] = 0
26 | return JsonResponse({'a': 1, 'session': session.dump()})
27 |
28 |
29 | if __name__ == '__main__':
30 | app.run(debug=True, port=8000, host='0.0.0.0')
31 |
--------------------------------------------------------------------------------
/samples/sessions/files.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, JsonResponse, Request
2 | from vibora.hooks import Events
3 | from vibora.sessions.files import FilesSessionEngine
4 |
5 |
6 | app = Vibora()
7 |
8 |
9 | @app.handle(Events.BEFORE_SERVER_START)
10 | async def set_up_sessions(current_app: Vibora):
11 | current_app.session_engine = FilesSessionEngine('/tmp/test')
12 |
13 |
14 | @app.route('/')
15 | async def home(request: Request):
16 | session = await request.session()
17 | if 'requests_count' in session:
18 | session['requests_count'] += 1
19 | else:
20 | session['requests_count'] = 0
21 | return JsonResponse({'a': 1, 'session': session.dump()})
22 |
23 |
24 | if __name__ == '__main__':
25 | app.run(debug=False, port=8000, host='0.0.0.0')
26 |
--------------------------------------------------------------------------------
/samples/sessions/redis.py:
--------------------------------------------------------------------------------
1 | import asyncio_redis
2 | from vibora import Vibora
3 | from vibora.hooks import Events
4 | from vibora.sessions import AsyncRedis
5 | from vibora.responses import Response
6 |
7 |
8 | app = Vibora(
9 | sessions=AsyncRedis()
10 | )
11 |
12 |
13 | @app.route('/', cache=False)
14 | async def home(request):
15 | print(request.session.dump())
16 | await request.load_session()
17 | if request.session.get('count') is None:
18 | request.session['count'] = 0
19 | request.session['count'] += 1
20 | return Response(str(request.session['count']).encode())
21 |
22 |
23 | @app.handle(Events.BEFORE_SERVER_START)
24 | async def open_connections(loop):
25 | pool = await asyncio_redis.Pool.create(host='localhost', port=6379, poolsize=10)
26 | app.session_engine.connection = pool
27 |
28 |
29 | if __name__ == '__main__':
30 | app.run(debug=True, port=8000, host='0.0.0.0', workers=6)
31 |
--------------------------------------------------------------------------------
/samples/simple.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import Response
3 |
4 |
5 | app = Vibora()
6 |
7 |
8 | @app.route('/')
9 | async def home():
10 | return Response(b'123')
11 |
12 |
13 | if __name__ == '__main__':
14 | app.run(debug=False, port=8000)
15 |
--------------------------------------------------------------------------------
/samples/simple_json.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Request
2 | from vibora.responses import JsonResponse
3 | from vibora.schemas import Schema, fields
4 |
5 |
6 | class SimpleSchema(Schema):
7 |
8 | name: str
9 |
10 |
11 | app = Vibora()
12 |
13 |
14 | @app.route('/', methods=['POST'])
15 | async def home(request: Request):
16 | # schema = await SimpleSchema.load_json(request)
17 | return JsonResponse({'name': (await request.json())['name']})
18 |
19 |
20 | if __name__ == '__main__':
21 | app.run(debug=False, port=8000, host='0.0.0.0', workers=8)
22 |
--------------------------------------------------------------------------------
/samples/static.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import Response
3 | from vibora.static import StaticHandler
4 |
5 |
6 | app = Vibora(
7 | static=StaticHandler(paths=['/your_static_dir', '/second_static_dir'])
8 | )
9 |
10 |
11 | @app.route('/')
12 | async def home():
13 | return Response(b'123')
14 |
15 |
16 | if __name__ == '__main__':
17 | app.run(debug=False, port=8888)
18 |
--------------------------------------------------------------------------------
/samples/static/app.js:
--------------------------------------------------------------------------------
1 | alert("hello :)");
--------------------------------------------------------------------------------
/samples/streaming.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import StreamingResponse
3 |
4 |
5 | app = Vibora()
6 |
7 |
8 | @app.route('/', methods=['GET'])
9 | async def home():
10 | return StreamingResponse(b'123')
11 |
12 |
13 | if __name__ == '__main__':
14 | app.run(debug=False, port=8888, host='localhost')
15 |
--------------------------------------------------------------------------------
/samples/templates.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 |
3 |
4 | app = Vibora()
5 |
6 | t = [x for x in range(0, 5)]
7 |
8 |
9 | @app.route('/')
10 | async def home():
11 | return await app.render('index.html', teste=t)
12 |
13 |
14 | if __name__ == '__main__':
15 | app.run(debug=False, port=8000, host='0.0.0.0', workers=6)
16 |
--------------------------------------------------------------------------------
/samples/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test123
5 | {% static 'app.js' %}
6 |
7 |
8 | {% url 'home' %}
9 | {% for x in [1, 2, 3, 4] %}
10 | {{ x }}
11 | {% endfor %}
12 | {% block content %}{% endblock %}
13 |
14 |
--------------------------------------------------------------------------------
/samples/templates/header.html:
--------------------------------------------------------------------------------
1 | Include Static: {% static 'app.js' %}
2 | {{ teste }}
--------------------------------------------------------------------------------
/samples/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 | {% macro asd(value) %}
3 | {{ value }} aaaa + 1234
4 | {% endmacro %}
5 | {% block content %}
6 | qwe123456bs
7 | {% include 'header.html' %}
8 | {% for x in teste %}
9 | {{ x }}
10 | {% for y in teste %}
11 | {{ y }}
12 | {% endfor %}
13 | {% endfor %}
14 | {% endblock %}
--------------------------------------------------------------------------------
/samples/templates_cython.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 |
3 | app = Vibora()
4 |
5 | t = [x for x in range(0, 5)]
6 |
7 |
8 | @app.route('/')
9 | def home():
10 | return app.render('index.html', teste=t)
11 |
12 |
13 | if __name__ == '__main__':
14 | app.run(debug=False, port=8002, host='0.0.0.0')
15 |
--------------------------------------------------------------------------------
/samples/upload.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import Response
3 | from vibora.request import Request
4 |
5 |
6 | app = Vibora()
7 |
8 |
9 | @app.route('/', methods=['POST'])
10 | def home(request: Request):
11 | print(request.form)
12 | return Response(b'asd')
13 |
14 |
15 | if __name__ == '__main__':
16 | app.run(debug=False, port=8000, host='localhost')
17 |
--------------------------------------------------------------------------------
/samples/websockets.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.websockets import WebsocketHandler
3 |
4 |
5 | app = Vibora()
6 |
7 |
8 | @app.websocket('/')
9 | class ConnectedClient(WebsocketHandler):
10 | async def on_message(self, msg):
11 | print(msg)
12 | await self.send(msg)
13 |
14 | async def on_connect(self):
15 | print('Client connected')
16 |
17 |
18 | if __name__ == '__main__':
19 | app.run()
20 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 |
4 | [bdist_wheel]
5 | # This flag says that the code is written to work on both Python 2 and Python
6 | # 3. If at all possible, it is good practice to do this. If you cannot, you
7 | # will need to generate wheels for each Python version that you support.
8 | universal=0
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 | import pathlib
3 | from setuptools import setup, Extension, find_packages
4 |
5 | # Loading version
6 | here = pathlib.Path(__file__).parent
7 | txt = (here / 'vibora' / '__version__.py').read_text()
8 | version = re.findall(r"^__version__ = '([^']+)'\r?$", txt, re.M)[0]
9 |
10 |
11 | setup(
12 | name="vibora",
13 | version=version,
14 | description='Fast, asynchronous and efficient Python web framework',
15 | author='Frank Vieira',
16 | author_email='frank@frankvieira.com.br',
17 | url='https://vibora.io',
18 | license='MIT',
19 | python_requires='>=3.6',
20 | classifiers=[
21 | 'Intended Audience :: Developers',
22 | 'Development Status :: 4 - Alpha',
23 | 'Environment :: Web Environment',
24 | 'License :: OSI Approved :: MIT License',
25 | 'Programming Language :: Python :: 3.6'
26 | ],
27 | extras_require={
28 | 'dev': ['flake8', 'pytest', 'tox'],
29 | 'fast': ['ujson==1.35', 'uvloop==0.10.2']
30 | },
31 | ext_modules=[
32 | Extension(
33 | "vibora.parsers.parser",
34 | [
35 | "vibora/parsers/parser.c",
36 | "vendor/http-parser-2.8.1/http_parser.c",
37 | ],
38 | extra_compile_args=['-O3'],
39 | include_dirs=['.', '/git/vibora/vibora']
40 | ),
41 | Extension(
42 | "vibora.parsers.response",
43 | [
44 | "vibora/parsers/response.c",
45 | "vendor/http-parser-2.8.1/http_parser.c"
46 | ],
47 | extra_compile_args=['-O3'],
48 | include_dirs=['.', '/git/vibora/vibora']
49 | ),
50 | Extension(
51 | "vibora.router.router",
52 | ["vibora/router/router.c"],
53 | extra_compile_args=['-O3'],
54 | include_dirs=['.']
55 | ),
56 | Extension(
57 | "vibora.responses.responses",
58 | ["vibora/responses/responses.c"],
59 | extra_compile_args=['-O3'],
60 | include_dirs=['.']
61 | ),
62 | Extension(
63 | "vibora.protocol.cprotocol",
64 | ["vibora/protocol/cprotocol.c"],
65 | extra_compile_args=['-O3'],
66 | include_dirs=['.']
67 | ),
68 | Extension(
69 | "vibora.protocol.cwebsocket",
70 | ["vibora/protocol/cwebsocket.c"],
71 | extra_compile_args=['-O3'],
72 | include_dirs=['.']
73 | ),
74 | Extension(
75 | "vibora.request.request",
76 | ["vibora/request/request.c"],
77 | extra_compile_args=['-O3'],
78 | include_dirs=['.']
79 | ),
80 | Extension(
81 | "vibora.cache.cache",
82 | ["vibora/cache/cache.c"],
83 | extra_compile_args=['-O3'],
84 | include_dirs=['.']
85 | ),
86 | Extension(
87 | "vibora.headers.headers",
88 | ["vibora/headers/headers.c"],
89 | extra_compile_args=['-O3'],
90 | include_dirs=['.']
91 | ),
92 | Extension(
93 | "vibora.schemas.extensions.fields",
94 | ["vibora/schemas/extensions/fields.c"],
95 | extra_compile_args=['-O3'],
96 | include_dirs=['.', 'vibora/schemas']
97 | ),
98 | Extension(
99 | "vibora.schemas.extensions.schemas",
100 | ["vibora/schemas/extensions/schemas.c"],
101 | extra_compile_args=['-O3'],
102 | include_dirs=['.', 'vibora/schemas']
103 | ),
104 | Extension(
105 | "vibora.schemas.extensions.validator",
106 | ["vibora/schemas/extensions/validator.c"],
107 | extra_compile_args=['-O3'],
108 | include_dirs=['.', 'vibora/schemas']
109 | ),
110 | Extension(
111 | "vibora.components.components",
112 | ["vibora/components/components.c"],
113 | extra_compile_args=['-O3'],
114 | include_dirs=['.']
115 | ),
116 | Extension(
117 | "vibora.multipart.parser",
118 | ["vibora/multipart/parser.c"],
119 | extra_compile_args=['-O3'],
120 | include_dirs=['.']
121 | )
122 | ],
123 | packages=find_packages()
124 | )
125 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import os
2 | from unittest import TestLoader, TextTestRunner
3 |
4 |
5 | if __name__ == '__main__':
6 | loader = TestLoader()
7 | tests_dir = os.path.join(os.path.dirname(__file__), 'tests')
8 | tests = loader.discover(tests_dir, pattern='*.py')
9 | runner = TextTestRunner()
10 | result = runner.run(tests)
11 | if result.failures or result.errors:
12 | raise SystemExit(f'{len(result.failures) + len(result.errors)} tests failed.')
13 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/tests/__init__.py
--------------------------------------------------------------------------------
/tests/client/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/tests/client/__init__.py
--------------------------------------------------------------------------------
/tests/client/external.py:
--------------------------------------------------------------------------------
1 | from vibora import client
2 | from vibora.tests import TestSuite
3 |
4 |
5 | class HelloWorldCase(TestSuite):
6 |
7 | async def test_simple_get_google__expects_successful(self):
8 | response = await client.get('https://google.com/')
9 | self.assertEqual(response.status_code, 200)
10 |
11 | async def test_simple_get_google_https__expects_successful(self):
12 | response = await client.get('https://google.com/')
13 | self.assertEqual(response.status_code, 200)
14 |
--------------------------------------------------------------------------------
/tests/client/interface.py:
--------------------------------------------------------------------------------
1 | from vibora import client
2 | from vibora.client.exceptions import MissingSchema
3 | from vibora.tests import TestSuite
4 |
5 |
6 | class ClientInterfaceTestCase(TestSuite):
7 |
8 | async def test_form_and_body_combined__expects_exception(self):
9 | try:
10 | await client.post('http://google.com', form={}, body=b'')
11 | self.fail('Failed to prevent the user from doing wrong params combination.')
12 | except ValueError:
13 | pass
14 |
15 | async def test_form_and_json_combined__expects_exception(self):
16 | try:
17 | await client.post('http://google.com', form={}, json={})
18 | self.fail('Failed to prevent the user from doing wrong params combination.')
19 | except ValueError:
20 | pass
21 |
22 | async def test_body_and_json_combined__expects_exception(self):
23 | try:
24 | await client.post('http://google.com', body=b'', json={})
25 | self.fail('Failed to prevent the user from doing wrong params combination.')
26 | except ValueError:
27 | pass
28 |
29 | async def test_missing_schema_url__expects_missing_schema_exception(self):
30 | try:
31 | await client.get('google.com')
32 | self.fail('Missing schema exception not raised.')
33 | except MissingSchema:
34 | pass
35 |
--------------------------------------------------------------------------------
/tests/client/keep_alive.py:
--------------------------------------------------------------------------------
1 | from vibora.tests import TestSuite
2 | from vibora import Vibora
3 | from vibora.client import Session
4 | from vibora.utils import wait_server_offline
5 |
6 |
7 | class KeepAliveTestCase(TestSuite):
8 |
9 | async def test_connection_pool_recycling_connections(self):
10 | v = Vibora()
11 | address, port = '127.0.0.1', 65530
12 | async with Session(prefix=f'http://{address}:{port}', timeout=3, keep_alive=True) as client:
13 | v.run(host=address, port=port, block=False, necromancer=False, workers=1, debug=False,
14 | startup_message=False)
15 | self.assertEqual((await client.get('/')).status_code, 404)
16 | v.clean_up()
17 | wait_server_offline(address, port, timeout=30)
18 | v.run(host=address, port=port, block=False, necromancer=False, workers=1, debug=False,
19 | startup_message=False)
20 | self.assertEqual((await client.get('/')).status_code, 404)
21 |
--------------------------------------------------------------------------------
/tests/client/multipart.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Request
2 | from vibora.tests import TestSuite
3 | from vibora.responses import JsonResponse
4 | from vibora.multipart import FileUpload
5 |
6 |
7 | class FormsTestCase(TestSuite):
8 |
9 | async def test_simple_post__expects_correctly_interpreted(self):
10 | app = Vibora()
11 |
12 | @app.route('/', methods=['POST'])
13 | async def home(request: Request):
14 | return JsonResponse((await request.form()))
15 |
16 | async with app.test_client() as client:
17 | response = await client.post('/', form={'a': 1, 'b': 2})
18 |
19 | self.assertDictEqual(response.json(), {'a': '1', 'b': '2'})
20 |
21 | async def test_file_upload_with_another_values(self):
22 | app = Vibora()
23 |
24 | @app.route('/', methods=['POST'])
25 | async def home(request: Request):
26 | form = await request.form()
27 | return JsonResponse({'a': form['a'], 'b': (await form['b'].read()).decode()})
28 |
29 | async with app.test_client() as client:
30 | response = await client.post('/', form={'a': 1, 'b': FileUpload(content=b'uploaded_file')})
31 | self.assertDictEqual(response.json(), {'a': '1', 'b': 'uploaded_file'})
32 |
--------------------------------------------------------------------------------
/tests/client/ssl_connections.py:
--------------------------------------------------------------------------------
1 | import ssl
2 | import logging
3 | from vibora.tests import TestSuite
4 | from vibora import client
5 |
6 |
7 | class TestSSLErrors(TestSuite):
8 |
9 | def setUp(self):
10 | # Python always warns about SSL errors but since where are forcing them to occur
11 | # there is no reason to fill the testing console with these messages.
12 | logging.disable(logging.CRITICAL)
13 |
14 | def tearDown(self):
15 | logging.disable(logging.NOTSET)
16 |
17 | async def test_expired_ssl__expects_exception(self):
18 | try:
19 | await client.get('https://expired.badssl.com/')
20 | self.fail('Client trusted in an expired SSL certificate.')
21 | except ssl.SSLError:
22 | pass
23 |
24 | async def test_expired_ssl__expects_ignored(self):
25 | try:
26 | await client.get('https://expired.badssl.com/', ssl=False)
27 | except ssl.SSLError:
28 | self.fail('Client raised an exception for an expired SSL certificate '
29 | 'even when explicitly told to not do so.')
30 |
31 | async def test_wrong_host_ssl__expects_exception(self):
32 | try:
33 | await client.get('https://wrong.host.badssl.com/')
34 | self.fail('Client trusted in an SSL certificate with an invalid hostname.')
35 | except (ssl.CertificateError, ssl.SSLError):
36 | pass
37 |
38 | async def test_wrong_host_ssl__expects_ignored(self):
39 | try:
40 | await client.get('https://wrong.host.badssl.com/', ssl=False)
41 | except ssl.CertificateError:
42 | self.fail('Failed to ignore SSL verification.')
43 |
44 | async def test_self_signed_certificate__expects_exception(self):
45 | try:
46 | await client.get('https://self-signed.badssl.com/')
47 | self.fail('Client trusted in an self signed certificate.')
48 | except ssl.SSLError:
49 | pass
50 |
51 | async def test_self_signed_certificate__expects_ignored(self):
52 | try:
53 | await client.get('https://self-signed.badssl.com/', ssl=False)
54 | except ssl.SSLError:
55 | self.fail('Failed to ignore SSL verification.')
56 |
57 | async def test_untrusted_root_certificate__expects_exception(self):
58 | try:
59 | await client.get('https://untrusted-root.badssl.com/')
60 | self.fail('Client trusted in an untrusted root certificate.')
61 | except ssl.SSLError:
62 | pass
63 |
64 | async def test_untrusted_root_certificate__expects_ignored(self):
65 | try:
66 | await client.get('https://untrusted-root.badssl.com/', ssl=False)
67 | except ssl.SSLError:
68 | self.fail('Failed to ignore SSL verification.')
69 |
70 | async def test_trusted_certificate__expects_allowed(self):
71 | try:
72 | await client.get('https://google.com/')
73 | except ssl.SSLError:
74 | self.fail('Failed to validate Google certificate.')
75 |
76 | # Pending OCSP/CRL SSL implementation.
77 | # def test_revoked_certificate__expects_exception(self):
78 | # try:
79 | # http.get('https://revoked.badssl.com/')
80 | # self.fail('Client trusted in a revoked certificate.')
81 | # except ssl.SSLError:
82 | # pass
83 |
84 | # Pending OCSP/CRL SSL implementation.
85 | # def test_revoked_certificate__expects_ignored(self):
86 | # try:
87 | # http.get('https://revoked.badssl.com/', verify=False)
88 | # except ssl.SSLError:
89 | # self.fail('Client failed to ignore a revoked certificate.')
90 |
--------------------------------------------------------------------------------
/tests/client/streaming.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.tests import TestSuite
3 | from vibora.responses import StreamingResponse, Response
4 |
5 |
6 | class ChunkedStreamingTestCase(TestSuite):
7 |
8 | def setUp(self):
9 | def generate_data():
10 | yield b'1' * (10 * 1024)
11 | yield b'2' * 1024
12 | self.data = b''.join(generate_data())
13 | self.server = Vibora()
14 |
15 | @self.server.route('/')
16 | async def home():
17 | return StreamingResponse(generate_data)
18 |
19 | async def test_streaming_client_reading_content__expects_successful(self):
20 | async with self.server.test_client() as client:
21 | response = await client.get('/', stream=True)
22 | await response.read_content()
23 | self.assertEqual(response.content, self.data)
24 |
25 | async def test_streaming_client_reading_stream__expects_successful(self):
26 | async with self.server.test_client() as client:
27 | response = await client.get('/', stream=True)
28 | received_data = bytearray()
29 | async for chunk in response.stream():
30 | received_data.extend(chunk)
31 | self.assertEqual(received_data, self.data)
32 |
33 | async def test_streaming_client_very_small_reads__expects_successful(self):
34 | client = self.server.test_client()
35 | response = await client.get('/', stream=True)
36 | received_data = bytearray()
37 | async for chunk in response.stream(chunk_size=1):
38 | self.assertTrue(len(chunk) == 1)
39 | received_data.extend(chunk)
40 | self.assertEqual(received_data, self.data)
41 |
42 |
43 | class StreamingTestCase(TestSuite):
44 |
45 | def setUp(self):
46 | self.data = b'1' * (10 * 1024) + b'2' * 1024
47 | self.server = Vibora()
48 |
49 | @self.server.route('/')
50 | async def home():
51 | return Response(self.data)
52 |
53 | async def test_streaming_client_reading_content__expects_successful(self):
54 | client = self.server.test_client()
55 | response = await client.get('/', stream=True)
56 | await response.read_content()
57 | self.assertEqual(response.content, self.data)
58 |
59 | async def test_streaming_client_reading_stream__expects_successful(self):
60 | client = self.server.test_client()
61 | response = await client.get('/', stream=True)
62 | received_data = bytearray()
63 | async for chunk in response.stream():
64 | received_data.extend(chunk)
65 | self.assertEqual(received_data, self.data)
66 |
67 | async def test_streaming_client_very_small_reads__expects_successful(self):
68 | client = self.server.test_client()
69 | response = await client.get('/', stream=True)
70 | received_data = bytearray()
71 | async for chunk in response.stream(chunk_size=1):
72 | self.assertTrue(len(chunk) == 1)
73 | received_data.extend(chunk)
74 | self.assertEqual(received_data, self.data)
75 |
--------------------------------------------------------------------------------
/tests/forms.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.multipart import FileUpload
3 | from vibora.request import Request
4 | from vibora.responses import JsonResponse
5 | from vibora.tests import TestSuite
6 |
7 |
8 | class FormsTestCase(TestSuite):
9 |
10 | async def test_simple_form_expects_correctly_parsed(self):
11 | app = Vibora()
12 |
13 | @app.route('/', methods=['POST'])
14 | async def home(request: Request):
15 | form = await request.form()
16 | return JsonResponse(form)
17 |
18 | async with app.test_client() as client:
19 | response = await client.post('/', form={'a': 1, 'b': 2})
20 | self.assertEqual(response.status_code, 200)
21 | self.assertDictEqual(response.json(), {'a': '1', 'b': '2'})
22 |
23 | async def test_files_upload_expects_correctly_parsed(self):
24 | app = Vibora()
25 |
26 | @app.route('/', methods=['POST'])
27 | async def home(request: Request):
28 | form = await request.form()
29 | return JsonResponse({'a': (await form['a'].read()).decode(), 'b': (await form['b'].read()).decode(),
30 | 'c': (await form['c'].read()).decode(), 'd': form['d']})
31 |
32 | async with app.test_client() as client:
33 | response = await client.post(
34 | '/', form={
35 | 'a': FileUpload(content=b'a'),
36 | 'b': FileUpload(content=b'b'),
37 | 'c': FileUpload(content=b'c'),
38 | 'd': 1
39 | }
40 | )
41 | self.assertEqual(response.status_code, 200)
42 | self.assertDictEqual(response.json(), {'a': 'a', 'b': 'b', 'c': 'c', 'd': '1'})
43 |
44 | async def test_files_attribute_expects_correctly_parsed(self):
45 | app = Vibora()
46 |
47 | @app.route('/', methods=['POST'])
48 | async def home(request: Request):
49 | uploaded_files = {}
50 | for file in await request.files():
51 | uploaded_files[file.filename] = (await file.read()).decode()
52 | return JsonResponse(uploaded_files)
53 |
54 | async with app.test_client() as client:
55 | response = await client.post(
56 | '/', form={
57 | 'a': FileUpload(content=b'a', name='a'),
58 | 'b': FileUpload(content=b'b', name='b'),
59 | 'c': FileUpload(content=b'c', name='c'),
60 | 'd': 1
61 | }
62 | )
63 | self.assertEqual(response.status_code, 200)
64 | self.assertDictEqual(response.json(), {'a': 'a', 'b': 'b', 'c': 'c'})
65 |
--------------------------------------------------------------------------------
/tests/headers.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import uuid
3 | import json
4 | from vibora import Vibora
5 | from vibora.headers import Headers
6 | from vibora.responses import JsonResponse
7 | from vibora.request import Request
8 | from vibora.tests import TestSuite
9 |
10 |
11 | class HeadersTestCase(unittest.TestCase):
12 |
13 | def test_headers_obj(self):
14 | headers = Headers()
15 | headers['test'] = '1'
16 | headers['test'] = '2'
17 | headers['a'] = '3'
18 | self.assertEqual({'test': '2', 'a': '3'}, headers.dump())
19 |
20 |
21 | class IntegrationHeadersTestCase(TestSuite):
22 |
23 | async def test_extra_headers__expects_correctly_evaluated(self):
24 | app = Vibora()
25 |
26 | @app.route('/')
27 | async def get_headers(request: Request):
28 | return JsonResponse(request.headers.dump())
29 |
30 | client = app.test_client()
31 | token = str(uuid.uuid4())
32 | response = await client.get('/', headers={'x-access-token': token})
33 | response = json.loads(response.content)
34 | self.assertEqual(response.get('x-access-token'), token)
35 |
--------------------------------------------------------------------------------
/tests/helpers.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.tests import TestSuite
3 | from vibora.responses import Response
4 |
5 |
6 | class UrlForTestSuite(TestSuite):
7 |
8 | def setUp(self):
9 | self.app = Vibora()
10 |
11 | @self.app.route('/123')
12 | async def home():
13 | return Response(b'')
14 |
15 | self.app.initialize()
16 |
17 | def test_hello_world_situation(self):
18 | self.assertEqual(self.app.url_for('home'), '/123/')
19 |
--------------------------------------------------------------------------------
/tests/limits.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora
2 | from vibora.responses import Response
3 | from vibora.request import Request
4 | from vibora.tests import TestSuite
5 | from vibora.limits import ServerLimits, RouteLimits
6 |
7 |
8 | class LimitTestCase(TestSuite):
9 |
10 | async def test_body_smaller_than_limit_expects_200(self):
11 | app = Vibora(
12 | route_limits=RouteLimits(max_body_size=2)
13 | )
14 |
15 | @app.route('/', methods=['POST'])
16 | async def home(request: Request):
17 | await request.stream.read()
18 | return Response(b'Correct. Request should not be blocked.')
19 |
20 | async with app.test_client() as client:
21 | response = await client.post('/', body=b'1')
22 | self.assertEqual(response.status_code, 200)
23 |
24 | async def test_body_bigger_than_expected_expects_rejected(self):
25 | app = Vibora(
26 | route_limits=RouteLimits(max_body_size=1)
27 | )
28 |
29 | @app.route('/', methods=['POST'])
30 | async def home(request: Request):
31 | await request.stream.read()
32 | return Response(b'Wrong. Request should halted earlier.')
33 |
34 | async with app.test_client() as client:
35 | response = await client.post('/', body=b'12')
36 | self.assertEqual(response.status_code, 413)
37 |
38 | async def test_headers_bigger_than_expected_expects_rejected_request(self):
39 | app = Vibora(
40 | server_limits=ServerLimits(max_headers_size=1)
41 | )
42 |
43 | @app.route('/', methods=['GET'])
44 | async def home():
45 | return Response(b'Wrong. Request should halted earlier.')
46 |
47 | async with app.test_client() as client:
48 | response = await client.get('/')
49 | self.assertEqual(response.status_code, 400)
50 |
51 | async def test_headers_smaller_than_limit_expects_200(self):
52 | app = Vibora(
53 | server_limits=ServerLimits(max_headers_size=1 * 1024 * 1024)
54 | )
55 |
56 | @app.route('/', methods=['GET'])
57 | async def home():
58 | return Response(b'Correct. Request should pass without problems.')
59 |
60 | async with app.test_client() as client:
61 | response = await client.get('/')
62 | self.assertEqual(response.status_code, 200)
63 |
64 | async def test_custom_body_limit_per_route_expects_successful(self):
65 | app = Vibora(
66 | route_limits=RouteLimits(max_body_size=1)
67 | )
68 |
69 | @app.route('/', methods=['POST'], limits=RouteLimits(max_body_size=2))
70 | async def home(request: Request):
71 | await request.stream.read()
72 | return Response(b'Correct. Request should pass without problems.')
73 |
74 | async with app.test_client() as client:
75 | response = await client.post('/', body=b'11')
76 | self.assertEqual(response.status_code, 200)
77 |
78 | async def test_custom_body_limit_more_restrictive_per_route_expects_successful(self):
79 | app = Vibora(
80 | route_limits=RouteLimits(max_body_size=100)
81 | )
82 |
83 | @app.route('/', methods=['POST'], limits=RouteLimits(max_body_size=1))
84 | async def home(request: Request):
85 | await request.stream.read()
86 | return Response(b'Wrong. Request must be blocked because this route is more restrictive.')
87 |
88 | async with app.test_client() as client:
89 | response = await client.post('/', body=b'11')
90 | self.assertEqual(response.status_code, 413)
91 |
--------------------------------------------------------------------------------
/tests/responses.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from vibora.responses import JsonResponse, Response
3 | from vibora.cookies import Cookie
4 | from vibora.utils import json
5 |
6 |
7 | class AttributesTestCase(TestCase):
8 | """
9 | It's important to be able to access common attributes
10 | like cookies, headers from Python
11 | """
12 |
13 | def test_json_response_attributes(self):
14 | headers = {'test': 'test'}
15 | cookies = [Cookie('server', 'Vibora')]
16 | status_code = 404
17 | content = {'a': 1}
18 | response = JsonResponse(content, headers=headers, cookies=cookies, status_code=status_code)
19 | self.assertEqual(response.cookies, cookies)
20 | self.assertEqual(response.headers['test'], headers['test'])
21 | self.assertEqual(response.status_code, status_code)
22 | self.assertEqual(response.content, json.dumps(content).encode('utf-8'))
23 |
24 | def test_plain_response_attributes(self):
25 | headers = {'server': 'Vibora'}
26 | cookies = [Cookie('server', 'Vibora')]
27 | status_code = 404
28 | content = b'HelloWorld'
29 | response = Response(content, headers=headers, cookies=cookies, status_code=status_code)
30 | self.assertEqual(response.cookies, cookies)
31 | self.assertEqual(response.headers['server'], headers['server'])
32 | self.assertEqual(response.status_code, status_code)
33 | self.assertEqual(response.content, content)
34 |
--------------------------------------------------------------------------------
/tests/router/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/tests/router/__init__.py
--------------------------------------------------------------------------------
/tests/router/prefixes.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, TestSuite
2 | from vibora.blueprints import Blueprint
3 | from vibora.responses import JsonResponse
4 |
5 |
6 | class RouterPrefixesTestCase(TestSuite):
7 |
8 | async def test_root_route_expect_registered(self):
9 | data = {'hello': 'world'}
10 | app = Vibora()
11 |
12 | @app.route('/test', methods=['GET'])
13 | async def home():
14 | return JsonResponse(data)
15 |
16 | response = await app.test_client().request('/test/')
17 | self.assertEqual(response.json(), data)
18 |
19 | async def test_root_route_expect_not_found(self):
20 | app = Vibora()
21 | response = await app.test_client().request('/test')
22 | self.assertEqual(response.status_code, 404)
23 |
24 | async def test_add_blueprint_with_one_prefix(self):
25 | data, app = {'hello': 'world'}, Vibora()
26 | bp = Blueprint()
27 |
28 | @bp.route('/')
29 | async def home():
30 | return JsonResponse(data)
31 |
32 | app.add_blueprint(bp, prefixes={'test': '/test'})
33 | response = await app.test_client().request('/test')
34 | self.assertEqual(response.json(), data)
35 |
--------------------------------------------------------------------------------
/tests/schemas/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/tests/schemas/__init__.py
--------------------------------------------------------------------------------
/tests/streaming.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from asyncio import futures
3 | from vibora import Vibora
4 | from vibora.limits import ServerLimits
5 | from vibora.responses import StreamingResponse
6 | from vibora.tests import TestSuite
7 |
8 |
9 | class StreamingTestSuite(TestSuite):
10 |
11 | async def test_simple_streaming_expects_successful(self):
12 |
13 | app = Vibora()
14 |
15 | async def stream():
16 | for _ in range(0, 100):
17 | await asyncio.sleep(0.05)
18 | yield b'1'
19 |
20 | @app.route('/')
21 | async def home():
22 | return StreamingResponse(stream)
23 |
24 | async with app.test_client() as client:
25 | response = await client.get('/')
26 | self.assertEqual(response.status_code, 200)
27 | self.assertEqual(response.content, b'1' * 100)
28 |
29 | async def test_streaming_with_timeout__expects_timeout(self):
30 |
31 | app = Vibora()
32 |
33 | async def stream():
34 | for _ in range(0, 100):
35 | await asyncio.sleep(2)
36 | yield b'1'
37 |
38 | @app.route('/')
39 | async def home():
40 | return StreamingResponse(stream, complete_timeout=1)
41 |
42 | async with app.test_client() as client:
43 | try:
44 | await client.get('/', timeout=3)
45 | self.fail('Vibora should have closed the connection because a streaming timeout is not recoverable.')
46 | except asyncio.IncompleteReadError:
47 | pass
48 | except futures.TimeoutError:
49 | pass
50 |
51 | async def test_simple_streaming_with_chunk_timeout(self):
52 |
53 | app = Vibora(server_limits=ServerLimits(write_buffer=1))
54 |
55 | async def stream():
56 | for _ in range(0, 5):
57 | await asyncio.sleep(0)
58 | yield b'1' * 1024 * 1024 * 100
59 |
60 | @app.route('/')
61 | async def home():
62 | return StreamingResponse(stream, chunk_timeout=3, complete_timeout=999)
63 |
64 | async with app.test_client() as client:
65 | response = await client.get('/', stream=True)
66 | try:
67 | first = True
68 | chunk_size = 1 * 1024 * 1024
69 | async for chunk in response.stream(chunk_size=chunk_size):
70 | if first:
71 | await asyncio.sleep(5)
72 | first = False
73 | self.assertTrue(len(chunk) <= chunk_size)
74 | self.fail('Vibora should have closed the connection because of a chunk timeout.')
75 | except asyncio.IncompleteReadError:
76 | pass
77 |
--------------------------------------------------------------------------------
/tests/subdomains.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Response
2 | from vibora.blueprints import Blueprint
3 | from vibora.tests import TestSuite
4 |
5 |
6 | class BlueprintsTestCase(TestSuite):
7 |
8 | def setUp(self):
9 | self.app = Vibora()
10 |
11 | async def test_simple_sub_domain_expects_match(self):
12 | b1 = Blueprint(hosts=['.*'])
13 |
14 | @b1.route('/')
15 | async def home():
16 | return Response(b'123')
17 |
18 | self.app.add_blueprint(b1)
19 | async with self.app.test_client() as client:
20 | response = await client.request('/')
21 | self.assertEqual(response.content, b'123')
22 |
23 | async def test_exact_match_sub_domain_expects_match(self):
24 | b1 = Blueprint(hosts=['test.vibora.io'])
25 |
26 | @b1.route('/')
27 | async def home():
28 | return Response(b'123')
29 |
30 | self.app.add_blueprint(b1)
31 | async with self.app.test_client() as client:
32 | response = await client.request('/', headers={'Host': 'test.vibora.io'})
33 | self.assertEqual(response.content, b'123')
34 |
35 | async def test_different_sub_domain_expects_404(self):
36 | b1 = Blueprint(hosts=['test.vibora.io'])
37 |
38 | @b1.route('/')
39 | async def home():
40 | return Response(b'123')
41 |
42 | self.app.add_blueprint(b1)
43 | async with self.app.test_client() as client:
44 | response = await client.request('/', headers={'Host': 'test2.vibora.io'})
45 | self.assertEqual(response.status_code, 404)
46 |
47 | async def test_sub_domain_working_with_non_hosts_based(self):
48 | b1 = Blueprint(hosts=['test.vibora.io'])
49 | b2 = Blueprint()
50 |
51 | @b1.route('/')
52 | async def home():
53 | return Response(b'123')
54 |
55 | @b2.route('/test')
56 | async def home():
57 | return Response(b'123')
58 |
59 | self.app.add_blueprint(b1)
60 | self.app.add_blueprint(b2)
61 | async with self.app.test_client() as client:
62 | response = await client.request('/', headers={'Host': 'test.vibora.io'})
63 | self.assertEqual(response.status_code, 200)
64 | response = await client.request('/', headers={'Host': 'test2.vibora.io'})
65 | self.assertEqual(response.status_code, 404)
66 | response = await client.request('/test', headers={'Host': 'anything.should.work'})
67 | self.assertEqual(response.status_code, 200)
68 | response = await client.request('/test2', headers={'Host': 'anything.should.404'})
69 | self.assertEqual(response.status_code, 404)
70 |
--------------------------------------------------------------------------------
/tests/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/tests/templates/__init__.py
--------------------------------------------------------------------------------
/tests/templates/exceptions.py:
--------------------------------------------------------------------------------
1 | from vibora.templates import Template, TemplateEngine
2 | from vibora.templates.exceptions import TemplateRenderError
3 | from vibora.tests import TestSuite
4 |
5 |
6 | class NodesParsingSuite(TestSuite):
7 |
8 | def setUp(self):
9 | self.template_engine = TemplateEngine()
10 |
11 | async def test_simple_exception_expects_correct_line_in_stack(self):
12 | """
13 |
14 | :return:
15 | """
16 | content = """
17 | {% for x in range(0, 10)%}
18 | {{ x.non_existent_call() }}
19 | {% endfor %}
20 | """
21 | self.template_engine.add_template(Template(content=content), names=['test'])
22 | self.template_engine.compile_templates(verbose=False)
23 | try:
24 | await self.template_engine.render('test')
25 | except TemplateRenderError as error:
26 | self.assertEqual(error.template_line, '{{ x.non_existent_call() }}')
27 | self.assertIsInstance(error.original_exception, AttributeError)
28 |
--------------------------------------------------------------------------------
/tests/templates/extensions.py:
--------------------------------------------------------------------------------
1 | from vibora import Vibora, Response
2 | from vibora.tests import TestSuite
3 | from vibora.templates import Template
4 |
5 |
6 | class ViboraExtensionSuiteCase(TestSuite):
7 |
8 | def setUp(self):
9 | self.app = Vibora()
10 |
11 | @self.app.route('/')
12 | async def home():
13 | return Response(b'')
14 |
15 | self.app.initialize()
16 |
17 | async def test_url_for_inside_for_node(self):
18 | template = Template('{% for x in range(0, 10)%}{% url "home" %}{% endfor %}')
19 | self.app.template_engine.add_template(template, ['test'])
20 | self.app.template_engine.compile_templates()
21 | self.assertEqual(self.app.url_for('home') * 10, await self.app.template_engine.render('test'))
22 |
23 | async def test_static_node(self):
24 | template = Template("{% static 'js/app.js' %}")
25 | self.app.template_engine.add_template(template, ['test'])
26 | self.app.template_engine.compile_templates()
27 | self.assertEqual('/static/js/app.js', await self.app.template_engine.render('test'))
28 |
--------------------------------------------------------------------------------
/tests/templates/nodes.py:
--------------------------------------------------------------------------------
1 | from collections import deque
2 | from vibora.templates import Template, TemplateParser, ForNode, EvalNode, TextNode, IfNode, ElseNode
3 | from vibora.tests import TestSuite
4 |
5 |
6 | class NodesParsingSuite(TestSuite):
7 |
8 | def test_for_node(self):
9 | """
10 |
11 | :return:
12 | """
13 | tp = TemplateParser()
14 | parsed = tp.parse(Template(content='{% for x in range(0, 10)%}{{x}}{%endfor %}'))
15 | expected_types = deque([ForNode, EvalNode])
16 | generated_nodes = parsed.flat_view(parsed.ast)
17 | while expected_types:
18 | expected_type = expected_types.popleft()
19 | current_node = next(generated_nodes)
20 | self.assertIsInstance(current_node, expected_type)
21 |
22 | def test_for_node_with_text_between(self):
23 | """
24 |
25 | :return:
26 | """
27 | tp = TemplateParser()
28 | parsed = tp.parse(Template(content='{% for x in range(0, 10)%} {{x}} {%endfor %}'))
29 | expected_types = deque([ForNode, TextNode, EvalNode, TextNode])
30 | generated_nodes = parsed.flat_view(parsed.ast)
31 | while expected_types:
32 | expected_type = expected_types.popleft()
33 | current_node = next(generated_nodes)
34 | self.assertIsInstance(current_node, expected_type)
35 |
36 | def test_for_node_with_if_condition(self):
37 | """
38 |
39 | :return:
40 | """
41 | tp = TemplateParser()
42 | parsed = tp.parse(Template(content='{% for x in range(0, 10)%}{% if x == 0 %}{{ y }}{% endif %}{% endfor %}'))
43 | expected_types = deque([ForNode, IfNode, EvalNode])
44 | generated_nodes = parsed.flat_view(parsed.ast)
45 | while expected_types:
46 | expected_type = expected_types.popleft()
47 | current_node = next(generated_nodes)
48 | self.assertIsInstance(current_node, expected_type)
49 |
50 | def test_for_node_with_if_else_condition(self):
51 | """
52 |
53 | :return:
54 | """
55 | tp = TemplateParser()
56 | content = """
57 | {% for x in range(0, 10)%}
58 | {% if x == 0 %}
59 | {{ x }}
60 | {% else %}
61 | -
62 | {% endif %}
63 | {% endfor %}
64 | """.replace('\n', '').replace(' ', '')
65 | parsed = tp.parse(Template(content=content))
66 | expected_types = deque([ForNode, IfNode, EvalNode, ElseNode, TextNode])
67 | generated_nodes = parsed.flat_view(parsed.ast)
68 | while expected_types:
69 | expected_type = expected_types.popleft()
70 | current_node = next(generated_nodes)
71 | self.assertIsInstance(current_node, expected_type)
72 |
--------------------------------------------------------------------------------
/tests/templates/render.py:
--------------------------------------------------------------------------------
1 | from vibora.tests import TestSuite
2 | from vibora.templates import TemplateEngine, Template
3 |
4 |
5 | class RenderSuite(TestSuite):
6 |
7 | def setUp(self):
8 | self.engine = TemplateEngine()
9 |
10 | async def test_empty_template_expects_empty_string(self):
11 | template = Template('')
12 | self.engine.add_template(template, ['test'])
13 | self.engine.compile_templates(verbose=False)
14 | self.assertEqual('', await self.engine.render('test'))
15 |
16 | async def test_render_for_with_with_unpacked_variables(self):
17 | template = Template('{% for a, b in [(1, 2)] %} {{ a }} + {{ b }} {% endfor %}')
18 | self.engine.add_template(template, ['test'])
19 | self.engine.compile_templates(verbose=False)
20 | self.assertEqual(' 1 + 2 ', await self.engine.render('test'))
21 |
22 | async def test_render_for_with_a_single_variable(self):
23 | template = Template('{% for a in [(1, 2)] %} {{ a }} {% endfor %}')
24 | self.engine.add_template(template, ['test'])
25 | self.engine.compile_templates(verbose=False)
26 | self.assertEqual(' (1, 2) ', await self.engine.render('test'))
27 |
--------------------------------------------------------------------------------
/tests/timeouts.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from vibora import Vibora
3 | from vibora.limits import RouteLimits
4 | from vibora.tests import TestSuite
5 | from vibora.responses import Response, StreamingResponse
6 |
7 |
8 | class TimeoutsTestCase(TestSuite):
9 |
10 | async def test_simple_case_expects_timeout_response(self):
11 |
12 | app = Vibora()
13 |
14 | @app.route('/', limits=RouteLimits(timeout=2))
15 | async def home():
16 | await asyncio.sleep(10)
17 | return Response(b'Wrong. This request should timeout.')
18 |
19 | async with app.test_client() as client:
20 | response = await client.get('/', timeout=4)
21 | self.assertEqual(response.status_code, 500)
22 |
23 | async def test_non_timeout_case_expects_successful_response(self):
24 |
25 | app = Vibora()
26 |
27 | @app.route('/', limits=RouteLimits(timeout=1))
28 | async def home():
29 | return Response(b'Correct.')
30 |
31 | async with app.test_client() as client:
32 | response = await client.get('/', timeout=4)
33 | self.assertEqual(response.status_code, 200)
34 |
35 | # We wait to see if the server correctly removed the timeout watcher
36 | # otherwise an exception will raise.
37 | await asyncio.sleep(2)
38 |
39 | async def test_async_streaming_expects_successful(self):
40 |
41 | app = Vibora()
42 |
43 | async def generator():
44 | yield b'1'
45 | await asyncio.sleep(2)
46 | yield b'2'
47 |
48 | @app.route('/', limits=RouteLimits(timeout=1))
49 | async def home():
50 | return StreamingResponse(generator)
51 |
52 | async with app.test_client() as client:
53 | try:
54 | await client.get('/', timeout=10)
55 | except Exception as error:
56 | print(error)
57 | self.fail("Timeout should be canceled because it's a streaming response.")
58 |
59 | async def test_async_streaming_expects_successful_response(self):
60 |
61 | app = Vibora()
62 |
63 | async def generator():
64 | yield b'1'
65 | await asyncio.sleep(0)
66 | yield b'2'
67 | await asyncio.sleep(0)
68 | yield b'3'
69 |
70 | @app.route('/', limits=RouteLimits(timeout=5))
71 | async def home():
72 | return StreamingResponse(generator)
73 |
74 | async with app.test_client() as client:
75 | response = await client.get('/')
76 | self.assertEqual(response.content, b'123')
77 |
78 | async def test_sync_iterator_expects_successful_response(self):
79 |
80 | app = Vibora()
81 |
82 | def generator():
83 | yield b'1'
84 | yield b'2'
85 | yield b'3'
86 |
87 | @app.route('/', limits=RouteLimits(timeout=5))
88 | async def home():
89 | return StreamingResponse(generator)
90 |
91 | async with app.test_client() as client:
92 | response = await client.get('/')
93 | self.assertEqual(response.content, b'123')
94 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/.gitignore:
--------------------------------------------------------------------------------
1 | /out/
2 | core
3 | tags
4 | *.o
5 | test
6 | test_g
7 | test_fast
8 | bench
9 | url_parser
10 | parsertrace
11 | parsertrace_g
12 | *.mk
13 | *.Makefile
14 | *.so.*
15 | *.exe.*
16 | *.exe
17 | *.a
18 |
19 |
20 | # Visual Studio uglies
21 | *.suo
22 | *.sln
23 | *.vcxproj
24 | *.vcxproj.filters
25 | *.vcxproj.user
26 | *.opensdf
27 | *.ncrunchsolution*
28 | *.sdf
29 | *.vsp
30 | *.psess
31 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/.mailmap:
--------------------------------------------------------------------------------
1 | # update AUTHORS with:
2 | # git log --all --reverse --format='%aN <%aE>' | perl -ne 'BEGIN{print "# Authors ordered by first contribution.\n"} print unless $h{$_}; $h{$_} = 1' > AUTHORS
3 | Ryan Dahl
4 | Salman Haq
5 | Simon Zimmermann
6 | Thomas LE ROUX LE ROUX Thomas
7 | Thomas LE ROUX Thomas LE ROUX
8 | Fedor Indutny
9 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/.travis.yml:
--------------------------------------------------------------------------------
1 | language: c
2 |
3 | compiler:
4 | - clang
5 | - gcc
6 |
7 | script:
8 | - "make"
9 |
10 | notifications:
11 | email: false
12 | irc:
13 | - "irc.freenode.net#node-ci"
14 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/AUTHORS:
--------------------------------------------------------------------------------
1 | # Authors ordered by first contribution.
2 | Ryan Dahl
3 | Jeremy Hinegardner
4 | Sergey Shepelev
5 | Joe Damato
6 | tomika
7 | Phoenix Sol
8 | Cliff Frey
9 | Ewen Cheslack-Postava
10 | Santiago Gala
11 | Tim Becker
12 | Jeff Terrace
13 | Ben Noordhuis
14 | Nathan Rajlich
15 | Mark Nottingham
16 | Aman Gupta
17 | Tim Becker
18 | Sean Cunningham
19 | Peter Griess
20 | Salman Haq
21 | Cliff Frey
22 | Jon Kolb
23 | Fouad Mardini
24 | Paul Querna
25 | Felix Geisendörfer
26 | koichik
27 | Andre Caron
28 | Ivo Raisr
29 | James McLaughlin
30 | David Gwynne
31 | Thomas LE ROUX
32 | Randy Rizun
33 | Andre Louis Caron
34 | Simon Zimmermann
35 | Erik Dubbelboer
36 | Martell Malone
37 | Bertrand Paquet
38 | BogDan Vatra
39 | Peter Faiman
40 | Corey Richardson
41 | Tóth Tamás
42 | Cam Swords
43 | Chris Dickinson
44 | Uli Köhler
45 | Charlie Somerville
46 | Patrik Stutz
47 | Fedor Indutny
48 | runner
49 | Alexis Campailla
50 | David Wragg
51 | Vinnie Falco
52 | Alex Butum
53 | Rex Feng
54 | Alex Kocharin
55 | Mark Koopman
56 | Helge Heß
57 | Alexis La Goutte
58 | George Miroshnykov
59 | Maciej Małecki
60 | Marc O'Morain
61 | Jeff Pinner
62 | Timothy J Fontaine
63 | Akagi201
64 | Romain Giraud
65 | Jay Satiro
66 | Arne Steen
67 | Kjell Schubert
68 | Olivier Mengué
69 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright Joyent, Inc. and other Node contributors.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to
5 | deal in the Software without restriction, including without limitation the
6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | sell copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/bench.c:
--------------------------------------------------------------------------------
1 | /* Copyright Fedor Indutny. All rights reserved.
2 | *
3 | * Permission is hereby granted, free of charge, to any person obtaining a copy
4 | * of this software and associated documentation files (the "Software"), to
5 | * deal in the Software without restriction, including without limitation the
6 | * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 | * sell copies of the Software, and to permit persons to whom the Software is
8 | * furnished to do so, subject to the following conditions:
9 | *
10 | * The above copyright notice and this permission notice shall be included in
11 | * all copies or substantial portions of the Software.
12 | *
13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
19 | * IN THE SOFTWARE.
20 | */
21 | #include "http_parser.h"
22 | #include
23 | #include
24 | #include
25 | #include
26 | #include
27 |
28 | /* 8 gb */
29 | static const int64_t kBytes = 8LL << 30;
30 |
31 | static const char data[] =
32 | "POST /joyent/http-parser HTTP/1.1\r\n"
33 | "Host: github.com\r\n"
34 | "DNT: 1\r\n"
35 | "Accept-Encoding: gzip, deflate, sdch\r\n"
36 | "Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4\r\n"
37 | "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) "
38 | "AppleWebKit/537.36 (KHTML, like Gecko) "
39 | "Chrome/39.0.2171.65 Safari/537.36\r\n"
40 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,"
41 | "image/webp,*/*;q=0.8\r\n"
42 | "Referer: https://github.com/joyent/http-parser\r\n"
43 | "Connection: keep-alive\r\n"
44 | "Transfer-Encoding: chunked\r\n"
45 | "Cache-Control: max-age=0\r\n\r\nb\r\nhello world\r\n0\r\n";
46 | static const size_t data_len = sizeof(data) - 1;
47 |
48 | static int on_info(http_parser* p) {
49 | return 0;
50 | }
51 |
52 |
53 | static int on_data(http_parser* p, const char *at, size_t length) {
54 | return 0;
55 | }
56 |
57 | static http_parser_settings settings = {
58 | .on_message_begin = on_info,
59 | .on_headers_complete = on_info,
60 | .on_message_complete = on_info,
61 | .on_header_field = on_data,
62 | .on_header_value = on_data,
63 | .on_url = on_data,
64 | .on_status = on_data,
65 | .on_body = on_data
66 | };
67 |
68 | int bench(int iter_count, int silent) {
69 | struct http_parser parser;
70 | int i;
71 | int err;
72 | struct timeval start;
73 | struct timeval end;
74 |
75 | if (!silent) {
76 | err = gettimeofday(&start, NULL);
77 | assert(err == 0);
78 | }
79 |
80 | fprintf(stderr, "req_len=%d\n", (int) data_len);
81 | for (i = 0; i < iter_count; i++) {
82 | size_t parsed;
83 | http_parser_init(&parser, HTTP_REQUEST);
84 |
85 | parsed = http_parser_execute(&parser, &settings, data, data_len);
86 | assert(parsed == data_len);
87 | }
88 |
89 | if (!silent) {
90 | double elapsed;
91 | double bw;
92 | double total;
93 |
94 | err = gettimeofday(&end, NULL);
95 | assert(err == 0);
96 |
97 | fprintf(stdout, "Benchmark result:\n");
98 |
99 | elapsed = (double) (end.tv_sec - start.tv_sec) +
100 | (end.tv_usec - start.tv_usec) * 1e-6f;
101 |
102 | total = (double) iter_count * data_len;
103 | bw = (double) total / elapsed;
104 |
105 | fprintf(stdout, "%.2f mb | %.2f mb/s | %.2f req/sec | %.2f s\n",
106 | (double) total / (1024 * 1024),
107 | bw / (1024 * 1024),
108 | (double) iter_count / elapsed,
109 | elapsed);
110 |
111 | fflush(stdout);
112 | }
113 |
114 | return 0;
115 | }
116 |
117 | int main(int argc, char** argv) {
118 | int64_t iterations;
119 |
120 | iterations = kBytes / (int64_t) data_len;
121 | if (argc == 2 && strcmp(argv[1], "infinite") == 0) {
122 | for (;;)
123 | bench(iterations, 1);
124 | return 0;
125 | } else {
126 | return bench(iterations, 0);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/vendor/http-parser-2.8.1/http_parser.gyp:
--------------------------------------------------------------------------------
1 | # This file is used with the GYP meta build system.
2 | # http://code.google.com/p/gyp/
3 | # To build try this:
4 | # svn co http://gyp.googlecode.com/svn/trunk gyp
5 | # ./gyp/gyp -f make --depth=`pwd` http_parser.gyp
6 | # ./out/Debug/test
7 | {
8 | 'target_defaults': {
9 | 'default_configuration': 'Debug',
10 | 'configurations': {
11 | # TODO: hoist these out and put them somewhere common, because
12 | # RuntimeLibrary MUST MATCH across the entire project
13 | 'Debug': {
14 | 'defines': [ 'DEBUG', '_DEBUG' ],
15 | 'cflags': [ '-Wall', '-Wextra', '-O0', '-g', '-ftrapv' ],
16 | 'msvs_settings': {
17 | 'VCCLCompilerTool': {
18 | 'RuntimeLibrary': 1, # static debug
19 | },
20 | },
21 | },
22 | 'Release': {
23 | 'defines': [ 'NDEBUG' ],
24 | 'cflags': [ '-Wall', '-Wextra', '-O3' ],
25 | 'msvs_settings': {
26 | 'VCCLCompilerTool': {
27 | 'RuntimeLibrary': 0, # static release
28 | },
29 | },
30 | }
31 | },
32 | 'msvs_settings': {
33 | 'VCCLCompilerTool': {
34 | },
35 | 'VCLibrarianTool': {
36 | },
37 | 'VCLinkerTool': {
38 | 'GenerateDebugInformation': 'true',
39 | },
40 | },
41 | 'conditions': [
42 | ['OS == "win"', {
43 | 'defines': [
44 | 'WIN32'
45 | ],
46 | }]
47 | ],
48 | },
49 |
50 | 'targets': [
51 | {
52 | 'target_name': 'http_parser',
53 | 'type': 'static_library',
54 | 'include_dirs': [ '.' ],
55 | 'direct_dependent_settings': {
56 | 'defines': [ 'HTTP_PARSER_STRICT=0' ],
57 | 'include_dirs': [ '.' ],
58 | },
59 | 'defines': [ 'HTTP_PARSER_STRICT=0' ],
60 | 'sources': [ './http_parser.c', ],
61 | 'conditions': [
62 | ['OS=="win"', {
63 | 'msvs_settings': {
64 | 'VCCLCompilerTool': {
65 | # Compile as C++. http_parser.c is actually C99, but C++ is
66 | # close enough in this case.
67 | 'CompileAs': 2,
68 | },
69 | },
70 | }]
71 | ],
72 | },
73 |
74 | {
75 | 'target_name': 'http_parser_strict',
76 | 'type': 'static_library',
77 | 'include_dirs': [ '.' ],
78 | 'direct_dependent_settings': {
79 | 'defines': [ 'HTTP_PARSER_STRICT=1' ],
80 | 'include_dirs': [ '.' ],
81 | },
82 | 'defines': [ 'HTTP_PARSER_STRICT=1' ],
83 | 'sources': [ './http_parser.c', ],
84 | 'conditions': [
85 | ['OS=="win"', {
86 | 'msvs_settings': {
87 | 'VCCLCompilerTool': {
88 | # Compile as C++. http_parser.c is actually C99, but C++ is
89 | # close enough in this case.
90 | 'CompileAs': 2,
91 | },
92 | },
93 | }]
94 | ],
95 | },
96 |
97 | {
98 | 'target_name': 'test-nonstrict',
99 | 'type': 'executable',
100 | 'dependencies': [ 'http_parser' ],
101 | 'sources': [ 'test.c' ]
102 | },
103 |
104 | {
105 | 'target_name': 'test-strict',
106 | 'type': 'executable',
107 | 'dependencies': [ 'http_parser_strict' ],
108 | 'sources': [ 'test.c' ]
109 | }
110 | ]
111 | }
112 |
--------------------------------------------------------------------------------
/vibora/__init__.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | try:
3 | import uvloop
4 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
5 | except ImportError:
6 | pass
7 | from .server import *
8 | from .tests import *
9 | from .responses import *
10 | from .request import Request
11 |
--------------------------------------------------------------------------------
/vibora/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.0.7'
2 |
--------------------------------------------------------------------------------
/vibora/cache/__init__.py:
--------------------------------------------------------------------------------
1 | from .cache import *
2 |
--------------------------------------------------------------------------------
/vibora/cache/cache.pxd:
--------------------------------------------------------------------------------
1 | # cython: language_level=3, boundscheck=False, wraparound=False, annotation_typing=False
2 |
3 | ###################################################
4 | # C IMPORTS
5 | # noinspection PyUnresolvedReferences
6 | from ..responses.responses cimport CachedResponse, Response
7 | # noinspection PyUnresolvedReferences
8 | from ..request.request cimport Request
9 | ###################################################
10 |
11 |
12 | cdef class CacheEngine:
13 | cdef:
14 | bint skip_hooks
15 | bint is_async
16 | readonly dict cache
17 |
18 | cpdef get(self, Request request)
19 | cpdef store(self, Request request, response)
20 |
21 |
22 |
23 | cdef class Static(CacheEngine):
24 | cpdef CachedResponse get(self, Request request)
25 |
--------------------------------------------------------------------------------
/vibora/cache/cache.py:
--------------------------------------------------------------------------------
1 | from inspect import iscoroutinefunction
2 | from ..responses.responses import CachedResponse, Response
3 | from ..request.request import Request
4 |
5 |
6 | class CacheEngine:
7 |
8 | def __init__(self, skip_hooks: bool=True):
9 | self.is_async = iscoroutinefunction(self.get) or iscoroutinefunction(self.store)
10 | self.skip_hooks = skip_hooks
11 | self.cache = {}
12 |
13 | def get(self, request: Request):
14 | raise NotImplementedError
15 |
16 | def store(self, request: Request, response: Response):
17 | raise NotImplementedError
18 |
19 |
20 | class Static(CacheEngine):
21 |
22 | def get(self, request: Request) -> CachedResponse:
23 | return self.cache.get(1)
24 |
25 | def store(self, request: Request, response: Response):
26 | self.cache[1] = CachedResponse(response.content, headers=response.headers, cookies=response.cookies)
27 |
--------------------------------------------------------------------------------
/vibora/client/__init__.py:
--------------------------------------------------------------------------------
1 | from .session import Session
2 | from .response import Response
3 | from . import retries as retries_module
4 |
5 |
6 | async def get(url: str = '', stream: bool = False, follow_redirects: bool = True, max_redirects: int = 30,
7 | decode: bool = True, ssl=None, timeout=None, retries: retries_module.RetryStrategy = None,
8 | headers: dict = None, query: dict = None,
9 | ignore_prefix: bool = False) -> Response:
10 | return await Session(keep_alive=False).request(url=url, stream=stream, follow_redirects=follow_redirects,
11 | max_redirects=max_redirects, decode=decode, ssl=ssl,
12 | retries=retries, headers=headers, timeout=timeout, method='GET',
13 | query=query, ignore_prefix=ignore_prefix)
14 |
15 |
16 | async def post(url: str = '', stream: bool = False, follow_redirects: bool = True, max_redirects: int = 30,
17 | decode: bool = True, validate_ssl=None, timeout=None, retries: retries_module.RetryStrategy = None,
18 | headers: dict = None, query: dict = None, body=None, form=None, json=None,
19 | ignore_prefix: bool = False) -> Response:
20 | return await Session(keep_alive=False).request(url=url, stream=stream, follow_redirects=follow_redirects,
21 | max_redirects=max_redirects, decode=decode, ssl=validate_ssl,
22 | retries=retries, headers=headers, timeout=timeout, method='POST',
23 | query=query,
24 | ignore_prefix=ignore_prefix, body=body, form=form, json=json)
25 |
--------------------------------------------------------------------------------
/vibora/client/connection.py:
--------------------------------------------------------------------------------
1 | import ssl
2 | from asyncio import StreamWriter, StreamReader, BaseEventLoop, wait_for, TimeoutError
3 | from typing import Coroutine
4 |
5 |
6 | SECURE_CONTEXT = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
7 | SECURE_CONTEXT.check_hostname = True
8 |
9 | INSECURE_CONTEXT = ssl.SSLContext()
10 | INSECURE_CONTEXT.check_hostname = False
11 |
12 |
13 | class Connection:
14 |
15 | __slots__ = ('loop', 'reader', 'writer', 'pool')
16 |
17 | def __init__(self, loop: BaseEventLoop, reader: StreamReader, writer: StreamWriter, pool):
18 | self.loop = loop
19 | self.reader = reader
20 | self.writer = writer
21 | self.pool = pool
22 |
23 | def sendall(self, data: bytes) -> Coroutine:
24 | """
25 |
26 | :param data:
27 | :return:
28 | """
29 | self.writer.write(data)
30 | return self.writer.drain()
31 |
32 | def read_exactly(self, length) -> Coroutine:
33 | """
34 |
35 | :param length:
36 | :return:
37 | """
38 | return self.reader.readexactly(length)
39 |
40 | def read_until(self, delimiter: bytes) -> Coroutine:
41 | """
42 |
43 | :param delimiter:
44 | :return:
45 | """
46 | return self.reader.readuntil(delimiter)
47 |
48 | def close(self):
49 | """
50 |
51 | :return:
52 | """
53 | self.writer.close()
54 |
55 | async def is_dropped(self):
56 | """
57 |
58 | :return:
59 | """
60 | try:
61 | await wait_for(self.reader.readexactly(0), 0.001)
62 | return True
63 | except TimeoutError:
64 | return False
65 |
66 | def release(self, keep_alive: bool=False):
67 | """
68 |
69 | :param keep_alive:
70 | :return:
71 | """
72 | self.pool.release_connection(self, keep_alive)
73 |
--------------------------------------------------------------------------------
/vibora/client/decoders.py:
--------------------------------------------------------------------------------
1 | import zlib
2 |
3 |
4 | class GzipDecoder(object):
5 |
6 | def __init__(self):
7 | self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
8 |
9 | def __getattr__(self, name):
10 | return getattr(self._obj, name)
11 |
12 | def decompress(self, data):
13 | if not data:
14 | return data
15 | return self._obj.decompress(data)
16 |
--------------------------------------------------------------------------------
/vibora/client/defaults.py:
--------------------------------------------------------------------------------
1 | from .retries import RetryStrategy
2 |
3 |
4 | class ClientDefaults:
5 | TIMEOUT = 30
6 | HEADERS = {
7 | 'User-Agent': 'Vibora',
8 | 'Accept': '*/*',
9 | 'Connection': 'keep-alive',
10 | 'Accept-Encoding': 'gzip, deflate'
11 | }
12 | RETRY_STRATEGY = RetryStrategy()
13 |
--------------------------------------------------------------------------------
/vibora/client/exceptions.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class HTTPClientError(Exception):
4 | pass
5 |
6 |
7 | class TooManyRedirects(HTTPClientError):
8 | pass
9 |
10 |
11 | class StreamAlreadyConsumed(HTTPClientError):
12 | pass
13 |
14 |
15 | class TooManyConnections(HTTPClientError):
16 | pass
17 |
18 |
19 | class RequestTimeout(HTTPClientError):
20 | pass
21 |
22 |
23 | class TooManyInvalidResponses(HTTPClientError):
24 | pass
25 |
26 |
27 | class MissingSchema(HTTPClientError):
28 | pass
29 |
--------------------------------------------------------------------------------
/vibora/client/limits.py:
--------------------------------------------------------------------------------
1 | import re
2 | import asyncio
3 | from time import time
4 |
5 |
6 | class RequestRate:
7 |
8 | __slots__ = ('_count', '_period', '_actual_usage', '_next_flush', 'pattern', 'optimistic')
9 |
10 | def __init__(self, count: int, period: int, pattern=None, optimistic: bool=False):
11 | """
12 |
13 | :param count: How many requests per period are allowed.
14 | If the request rate is 10 requests per minute than count=10 with period=60.
15 | :param period: How many seconds between each cycle.
16 | If the request rate is 10 requests per minute than the period is 60.
17 | :param pattern: A pattern to match against URLs and skip the rate for those that don't match the pattern.
18 | :param optimistic: Request rate is complicated because you never know how __exactly__ the server is measuring
19 | the throughput... By default Vibora is optimistic and assumes that the server will honor the rate/period
20 | accurately.
21 | """
22 | if isinstance(pattern, str):
23 | self.pattern = re.compile(pattern)
24 | else:
25 | self.pattern = pattern
26 | self.optimistic = optimistic
27 | self._count = count
28 | self._period = period
29 | self._actual_usage = 0
30 | self._next_flush = None
31 |
32 | async def notify(self):
33 | """
34 |
35 | :return:
36 | """
37 | now = time()
38 | if self._next_flush is None:
39 | self._next_flush = now + self._period
40 | elif now > self._next_flush:
41 | self._next_flush = now + self._period
42 | self._actual_usage = 0
43 | elif now < self._next_flush and self._actual_usage >= self._count:
44 | if self.optimistic:
45 | wait_time = (self._next_flush - now) or 0
46 | else:
47 | wait_time = self._period
48 | await asyncio.sleep(wait_time)
49 | self._actual_usage = 0
50 | self._actual_usage += 1
51 |
--------------------------------------------------------------------------------
/vibora/client/pool.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from asyncio import BaseEventLoop
3 | from collections import deque
4 | from .connection import Connection, SECURE_CONTEXT, INSECURE_CONTEXT
5 |
6 |
7 | class ConnectionPool:
8 |
9 | __slots__ = ('loop', 'host', 'port', 'protocol', 'max_connections', 'connections', 'available_connections',
10 | 'keep_alive', 'wait_connection_available')
11 |
12 | def __init__(self, loop: BaseEventLoop, host: str, port: int, protocol: str, keep_alive: bool=True):
13 | self.loop = loop
14 | self.host = host
15 | self.port = port
16 | self.protocol = protocol
17 | self.available_connections = deque()
18 | self.connections = set()
19 | self.keep_alive = keep_alive
20 |
21 | async def create_connection(self, ssl=None) -> Connection:
22 | """
23 |
24 | :param ssl:
25 | :return:
26 | """
27 | args = {'host': self.host, 'port': self.port, 'loop': self.loop}
28 | if self.protocol == 'https':
29 | if ssl is False:
30 | args['ssl'] = INSECURE_CONTEXT
31 | elif ssl is None:
32 | args['ssl'] = SECURE_CONTEXT
33 | else:
34 | args['ssl'] = ssl
35 | reader, writer = await asyncio.open_connection(**args)
36 | connection = Connection(self.loop, reader, writer, self)
37 | self.connections.add(connection)
38 | return connection
39 |
40 | async def get_connection(self, ssl) -> Connection:
41 | """
42 |
43 | :param ssl:
44 | :return:
45 | """
46 | try:
47 | connection = self.available_connections.pop()
48 | if not await connection.is_dropped():
49 | return connection
50 | else:
51 | await self.release_connection(connection, keep_alive=False)
52 | return await self.create_connection(ssl)
53 | except IndexError:
54 | return await self.create_connection(ssl)
55 |
56 | async def release_connection(self, connection: Connection, keep_alive=True):
57 | """
58 |
59 | :param connection:
60 | :param keep_alive:
61 | :return:
62 | """
63 | if keep_alive and self.keep_alive:
64 | self.available_connections.appendleft(connection)
65 | else:
66 | connection.close()
67 | self.connections.discard(connection)
68 |
69 | def close(self):
70 | """
71 |
72 | :return:
73 | """
74 | for connection in self.connections:
75 | connection.close()
76 |
--------------------------------------------------------------------------------
/vibora/client/request.py:
--------------------------------------------------------------------------------
1 | from vibora.parsers.typing import URL
2 | from ..cookies import CookiesJar
3 | from .connection import Connection
4 |
5 |
6 | class Request:
7 |
8 | __slots__ = ('method', 'url', 'headers', 'data', 'cookies', 'streaming', 'chunked',
9 | 'encoding', 'origin')
10 |
11 | def __init__(self, method: str, url: URL, headers: dict, data, cookies: CookiesJar, origin=None):
12 | self.method = method.upper() if method else 'GET'
13 | self.url = url
14 | self.headers = headers or {}
15 | if 'Host' not in headers:
16 | self.headers.update({'Host': url.host})
17 | self.data = data or b''
18 | self.cookies = cookies
19 | self.streaming = True if data and not isinstance(data, (bytes, bytearray)) else False
20 | self.chunked = self.streaming
21 | self.origin = origin
22 |
23 | async def encode(self, connection: Connection):
24 |
25 | # Headers
26 | http_request = f'{self.method} {self.url.path} HTTP/1.1\r\n'
27 | for header, value in self.headers.items():
28 | http_request += f'{header}: {str(value)}\r\n'
29 | if self.cookies:
30 | cookies = ';'.join([c.name + '=' + c.value for c in self.cookies])
31 | http_request += f'Cookie: {cookies}'
32 |
33 | if not self.streaming:
34 | http_request += f'Content-Length: {str(len(self.data))}\r\n'
35 | await connection.sendall((http_request + '\r\n').encode() + self.data)
36 |
37 | elif self.chunked:
38 | http_request += f'Transfer-Encoding: chunked\r\n'
39 | await connection.sendall((http_request + '\r\n').encode())
40 | for chunk in self.data:
41 | size = hex(len(chunk))[2:].encode('utf-8')
42 | await connection.sendall(size + b'\r\n' + chunk + b'\r\n')
43 | await connection.sendall(b'0\r\n\r\n')
44 |
45 | elif not self.chunked:
46 | body = bytearray()
47 | for chunk in self.data:
48 | body.extend(chunk)
49 | http_request += f'Content-Length: {str(len(body))}\r\n\r\n'
50 | body.extend(http_request.encode())
51 | await connection.sendall(body)
52 |
53 |
54 | class WebsocketRequest:
55 | def __init__(self, host: str, path: str='/', origin: str=None):
56 | self.host = host
57 | self.path = path
58 | self.headers = {
59 | 'Host': host,
60 | 'Connection': 'Upgrade',
61 | 'Upgrade': 'websocket',
62 | 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
63 | 'Sec-WebSocket-Version': '13'
64 | }
65 | if origin:
66 | self.headers['Origin'] = origin
67 |
68 | def encode(self):
69 | """
70 | GET ws://websocket.example.com/ HTTP/1.1
71 | Origin: http://example.com
72 | Connection: Upgrade
73 | Host: websocket.example.com
74 | Upgrade: websocket
75 | :return:
76 | """
77 | packet = f'GET wss://{self.host}{self.path} HTTP/1.1\r\n'
78 | for key, value in self.headers.items():
79 | packet += f'{key}: {value}\r\n'
80 | packet += '\r\n'
81 | return packet.encode()
82 |
--------------------------------------------------------------------------------
/vibora/client/retries.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class RetryStrategy:
4 |
5 | __slots__ = ('network_failures', 'responses')
6 |
7 | def __init__(self, network_failures: dict = None, responses: dict = None):
8 | """
9 |
10 | :param network_failures:
11 | :param responses:
12 | """
13 | # Although retry an operation after an connection reset is the default behavior of browsers
14 | # this is dangerous with non idempotent requests because it could duplicate charges/anything
15 | # in a poorly implemented API.
16 | # This issue is not rare when using connection pooling and the server does not tell us
17 | # about the keep alive timeout. There is no way to guarantee that a socket is still alive so
18 | # you write in it and hope for the best. In case the connection is reset after your next socket read
19 | # you never know if the server actually received it or not.
20 | self.network_failures = network_failures or {'GET': 1}
21 |
22 | # Retry after an specific status_code is found.
23 | self.responses = responses or {}
24 |
25 | def clone(self) -> 'RetryStrategy':
26 | return RetryStrategy(network_failures=self.network_failures.copy(), responses=self.responses.copy())
27 |
--------------------------------------------------------------------------------
/vibora/client/websocket.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from functools import partial
3 | from typing import Any, Coroutine
4 | from vibora.parsers.response import HttpResponseParser
5 | from vibora.websockets import FrameParser
6 | from .request import WebsocketRequest
7 |
8 |
9 | # https://websocket.org/echo.html
10 |
11 |
12 | class WebsocketProtocol(asyncio.Protocol):
13 |
14 | def __init__(self, transport, loop):
15 | self.loop = loop
16 | self.transport = transport
17 | self.parser = FrameParser(self)
18 |
19 | async def write(self, data):
20 | self.transport.write(data)
21 |
22 | def data_received(self, data):
23 | self.loop.create_task(self.parser.feed(data))
24 | print(f'WebsData received: {data}')
25 |
26 | async def on_message(self, data):
27 | print(f'Data {data}')
28 |
29 |
30 | class WebsocketHandshake(asyncio.Protocol):
31 |
32 | def __init__(self, client, loop):
33 | self.client = client
34 | self.loop = loop
35 | self.transport: asyncio.Transport = None
36 | self.parser = HttpResponseParser(self)
37 | self.current_status = None
38 | self.current_headers = None
39 |
40 | def connection_made(self, transport):
41 | """
42 |
43 | :param transport:
44 | :return:
45 | """
46 | wr = WebsocketRequest(self.client.host, path=self.client.path, origin=self.client.origin)
47 | transport.write(wr.encode())
48 | self.transport = transport
49 | print('connected')
50 |
51 | def data_received(self, data):
52 | self.parser.feed(data)
53 | print(f'Data received: {data}')
54 |
55 | def connection_lost(self, exc):
56 | print('The server closed the connection')
57 | print('Stop the event loop')
58 |
59 | # Parser Callbacks
60 | def on_body(self): pass
61 |
62 | def on_headers_complete(self, headers, status_code):
63 | self.current_status = status_code
64 | self.current_headers = headers
65 |
66 | def on_message_complete(self):
67 | self.transport.set_protocol(WebsocketProtocol(self.transport, self.loop))
68 |
69 |
70 | class WebsocketClient:
71 |
72 | def __init__(self, host: str, port: int, path: str = '/', loop=None, origin: str = None):
73 | self.host = host
74 | self.port = port
75 | self.path = path
76 | self.connected = False
77 | self.origin = origin
78 | self.loop = loop or asyncio.get_event_loop()
79 | self.buffer = bytearray()
80 |
81 | async def connect(self):
82 | factory = partial(WebsocketHandshake, self, self.loop)
83 | await self.loop.create_connection(factory, host=self.host, port=self.port, ssl=True)
84 |
85 | async def send(self, msg):
86 | if not self.connected:
87 | await self.connect()
88 | pass
89 |
90 | async def receive(self, max_size: int = 1 * 1024 * 1024, stream: bool = False):
91 | pass
92 |
--------------------------------------------------------------------------------
/vibora/components/__init__.py:
--------------------------------------------------------------------------------
1 | from .components import *
2 |
--------------------------------------------------------------------------------
/vibora/components/components.pxd:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class ComponentsEngine:
4 |
5 | cdef dict index
6 | cdef dict ephemeral_index
7 | cdef object request_class
8 | cdef void reset(self)
9 |
10 | cpdef ComponentsEngine clone(self)
11 |
--------------------------------------------------------------------------------
/vibora/components/context.py:
--------------------------------------------------------------------------------
1 | from typing import Type
2 | from asyncio import Task
3 |
4 |
5 | def get_component(type_: Type):
6 | """
7 | Get a component based .
8 | :param type_:
9 | :return:
10 | """
11 | current_task = Task.current_task()
12 | return current_task.components.get(type_)
13 |
--------------------------------------------------------------------------------
/vibora/constants.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | ALL_STATUS_CODES = {
4 | 100: 'Continue',
5 | 101: 'Switching Protocols',
6 | 102: 'Processing',
7 | 200: 'OK',
8 | 201: 'Created',
9 | 202: 'Accepted',
10 | 203: 'Non-Authoritative Information',
11 | 204: 'No Content',
12 | 205: 'Reset Content',
13 | 206: 'Partial Content',
14 | 207: 'Multi-Status',
15 | 208: 'Already Reported',
16 | 226: 'IM Used',
17 | 300: 'Multiple Choices',
18 | 301: 'Moved Permanently',
19 | 302: 'Found',
20 | 303: 'See Other',
21 | 304: 'Not Modified',
22 | 305: 'Use Proxy',
23 | 307: 'Temporary Redirect',
24 | 308: 'Permanent Redirect',
25 | 400: 'Bad Request',
26 | 401: 'Unauthorized',
27 | 402: 'Payment Required',
28 | 403: 'Forbidden',
29 | 404: 'Not Found',
30 | 405: 'Method Not Allowed',
31 | 406: 'Not Acceptable',
32 | 407: 'Proxy Authentication Required',
33 | 408: 'Request Timeout',
34 | 409: 'Conflict',
35 | 410: 'Gone',
36 | 411: 'Length Required',
37 | 412: 'Precondition Failed',
38 | 413: 'Request Entity Too Large',
39 | 414: 'Request-URI Too Long',
40 | 415: 'Unsupported Media Type',
41 | 416: 'Requested Range Not Satisfiable',
42 | 417: 'Expectation Failed',
43 | 422: 'Unprocessable Entity',
44 | 423: 'Locked',
45 | 424: 'Failed Dependency',
46 | 426: 'Upgrade Required',
47 | 428: 'Precondition Required',
48 | 429: 'Too Many Requests',
49 | 431: 'Request Header Fields Too Large',
50 | 500: 'Internal Server Error',
51 | 501: 'Not Implemented',
52 | 502: 'Bad Gateway',
53 | 503: 'Service Unavailable',
54 | 504: 'Gateway Timeout',
55 | 505: 'HTTP Version Not Supported',
56 | 506: 'Variant Also Negotiates',
57 | 507: 'Insufficient Storage',
58 | 508: 'Loop Detected',
59 | 510: 'Not Extended',
60 | 511: 'Network Authentication Required'
61 | }
62 |
--------------------------------------------------------------------------------
/vibora/context.py:
--------------------------------------------------------------------------------
1 | from asyncio import Task
2 | from .router import Route
3 |
4 |
5 | def get_component(type_):
6 | """
7 |
8 | :param type_:
9 | :return:
10 | """
11 | current_task = Task.current_task()
12 | return current_task.components.get(type_)
13 |
14 |
15 | def get_current_route() -> Route:
16 | """
17 |
18 | :return:
19 | """
20 | return get_component(Route)
21 |
--------------------------------------------------------------------------------
/vibora/exceptions.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | from typing import Callable, get_type_hints
3 |
4 |
5 | class ViboraException(Exception):
6 | pass
7 |
8 |
9 | class RouteConfigurationError(ViboraException):
10 | pass
11 |
12 |
13 | class MissingComponent(Exception):
14 | def __init__(self, msg, component=None, route=None):
15 | self.component = component
16 | self.route = route
17 | super().__init__(msg)
18 |
19 |
20 | class TemplateNotFound(ViboraException):
21 | pass
22 |
23 |
24 | class ReverseNotFound(ViboraException):
25 | def __init__(self, route_name):
26 | super().__init__('{0}\nCheck your function names.'.format(route_name))
27 |
28 |
29 | class InvalidJSON(ViboraException):
30 | pass
31 |
32 |
33 | class DuplicatedBlueprint(ViboraException):
34 | pass
35 |
36 |
37 | class ConflictingPrefixes(ViboraException):
38 | pass
39 |
40 |
41 | class ExceptionHandler:
42 | def __init__(self, handler: Callable, exception, local: bool=True):
43 | self.handler = handler
44 | self.exception = exception
45 | self.local = local
46 | self.params = self.extract_params()
47 |
48 | def call(self, components):
49 | params = {}
50 | for key, class_type in self.params:
51 | params[key] = components.get(class_type)
52 | return self.handler(**params)
53 |
54 | def extract_params(self):
55 | hints = get_type_hints(self.handler)
56 | if not hints and len(signature(self.handler).parameters) > 0:
57 | raise Exception(f'Type hint your handler ({self.handler}) params so Vibora can optimize stuff.')
58 | return tuple(filter(lambda x: x[0] != 'return', hints.items()))
59 |
60 |
61 | class NotFound(ViboraException):
62 | pass
63 |
64 |
65 | class StaticNotFound(ViboraException):
66 | pass
67 |
68 |
69 | class MethodNotAllowed(ViboraException):
70 | def __init__(self, allowed_methods: list):
71 | self.allowed_methods = allowed_methods
72 | super().__init__()
73 |
74 |
75 | class StreamAlreadyConsumed(ViboraException):
76 | def __init__(self):
77 | super().__init__('Stream already consumed')
78 |
--------------------------------------------------------------------------------
/vibora/headers/__init__.py:
--------------------------------------------------------------------------------
1 | from .headers import *
2 |
--------------------------------------------------------------------------------
/vibora/headers/headers.pxd:
--------------------------------------------------------------------------------
1 | # cython: language_level=3, boundscheck=False, wraparound=False, annotation_typing=False
2 | import cython
3 |
4 | @cython.freelist(1024)
5 | cdef class Headers:
6 |
7 | cdef dict values
8 | cdef list raw
9 | cdef bint evaluated
10 |
11 | cpdef get(self, str key, object default=*)
12 | cpdef eval(self)
13 | cpdef dump(self)
14 |
15 | cdef dict parse_cookies(self)
16 |
--------------------------------------------------------------------------------
/vibora/headers/headers.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class Headers:
4 |
5 | def __init__(self, raw=None):
6 | self.raw = raw or []
7 | self.values = None
8 | self.evaluated = False
9 |
10 | def get(self, key, default=None):
11 | if not self.evaluated:
12 | self.eval()
13 | return self.values.get(key.lower()) or default
14 |
15 | def eval(self):
16 | self.values = {}
17 | while self.raw:
18 | header = self.raw.pop()
19 | self.values[header[0].decode('utf-8').lower()] = header[1].decode('utf-8')
20 | self.evaluated = True
21 |
22 | def dump(self):
23 | if not self.evaluated:
24 | self.eval()
25 | return self.values
26 |
27 | def parse_cookies(self) -> dict:
28 | header = self.get('cookie')
29 | cookies = {}
30 | if header:
31 | for cookie in header.split(';'):
32 | first = cookie.find('=')
33 | name = cookie[:first].strip()
34 | value = cookie[first + 1:]
35 | cookies[name] = value
36 | return cookies
37 |
38 | def __getitem__(self, item: str):
39 | if not self.evaluated:
40 | self.eval()
41 | return self.values[item.lower()]
42 |
43 | def __setitem__(self, key: str, value: str):
44 | if not self.evaluated:
45 | self.eval()
46 | self.values[key.lower()] = value
47 |
48 | def __repr__(self):
49 | return f''
50 |
--------------------------------------------------------------------------------
/vibora/hooks.py:
--------------------------------------------------------------------------------
1 | from inspect import iscoroutinefunction
2 | from typing import get_type_hints
3 |
4 |
5 | class Events:
6 |
7 | # After the fork but before the server is online receiving requests.
8 | BEFORE_SERVER_START = 1
9 |
10 | # After server fork and server online. Understand that you could
11 | # theoretically be in the middle of a request while this function is running.
12 | # If you need to ensure something runs before the first request arrives "BEFORE_SERVER_START" is what you need.
13 | AFTER_SERVER_START = 2
14 |
15 | # Before each endpoint, you can halt requests, authorize users and many other common tasks
16 | # that are shared with many routes.
17 | BEFORE_ENDPOINT = 3
18 |
19 | # After each endpoint is called, this is useful when you need to globally inject data into responses
20 | # like headers.
21 | AFTER_ENDPOINT = 4
22 |
23 | # Called after each response is sent to the client, useful to cleaning and non-critical logging operations.
24 | AFTER_RESPONSE_SENT = 5
25 |
26 | # Called right before the server is stopped, you have the chance to cancel a stop request,
27 | # notify your clients and other useful stuff using this hook.
28 | BEFORE_SERVER_STOP = 6
29 |
30 | # Useful for debugging purposes.
31 | ALL = (BEFORE_SERVER_START, AFTER_SERVER_START, BEFORE_ENDPOINT, AFTER_ENDPOINT, AFTER_RESPONSE_SENT,
32 | BEFORE_SERVER_STOP)
33 |
34 |
35 | class Hook:
36 |
37 | __slots__ = ('event_type', 'handler', 'local', 'is_async', 'wanted_components')
38 |
39 | def __init__(self, event: int, handler, local=False):
40 | self.event_type = event
41 | self.handler = handler
42 | self.local = local
43 | self.is_async = iscoroutinefunction(handler)
44 | self.wanted_components = get_type_hints(self.handler)
45 |
46 | def call_handler(self, components):
47 | params = {}
48 | for name, required_type in self.wanted_components.items():
49 | params[name] = components.get(required_type)
50 | return self.handler(**params)
51 |
--------------------------------------------------------------------------------
/vibora/limits.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class ServerLimits:
4 |
5 | __slots__ = ('worker_timeout', 'keep_alive_timeout', 'response_timeout', 'max_body_size',
6 | 'max_headers_size', 'write_buffer')
7 |
8 | def __init__(self, worker_timeout: int=60, keep_alive_timeout: int=30,
9 | max_headers_size: int=1024 * 10, write_buffer: int=419430):
10 | """
11 |
12 | :param worker_timeout:
13 | :param keep_alive_timeout:
14 | :param max_headers_size:
15 | """
16 | self.worker_timeout = worker_timeout
17 | self.keep_alive_timeout = keep_alive_timeout
18 | self.max_headers_size = max_headers_size
19 | self.write_buffer = write_buffer
20 |
21 |
22 | class RouteLimits:
23 |
24 | __slots__ = ('timeout', 'max_body_size', 'in_memory_threshold')
25 |
26 | def __init__(self, max_body_size: int=1*1024*1024, timeout: int=30,
27 | in_memory_threshold: int=1*1024*1024):
28 | """
29 |
30 | :param max_body_size:
31 | :param timeout:
32 | """
33 | self.max_body_size = max_body_size
34 | self.timeout = timeout
35 | self.in_memory_threshold = in_memory_threshold
36 |
--------------------------------------------------------------------------------
/vibora/multipart/__init__.py:
--------------------------------------------------------------------------------
1 | from .parser import *
2 | from .containers import *
3 |
--------------------------------------------------------------------------------
/vibora/multipart/containers.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | import uuid
3 | import os
4 | from io import BytesIO
5 |
6 |
7 | class BufferedIterable:
8 | def __init__(self, item):
9 | self.item = item
10 | self.cursor = self.item.__iter__()
11 | self.buffer = bytearray()
12 |
13 | def read(self, size):
14 | while len(self.buffer) < size:
15 | self.buffer.extend(self.cursor.__next__())
16 | temp = self.buffer[:size + 1]
17 | self.buffer = self.buffer[size + 1:]
18 | return temp
19 |
20 |
21 | class FileUpload:
22 | def __init__(self, name: str=None, path: str=None, content: bytes=None, iterable=None,
23 | f=None, headers: list=None):
24 | if not any([path, content, iterable, f]):
25 | raise Exception('You must supply either: path, content, iterable, f')
26 | self.name = name
27 | if f:
28 | self.f = f
29 | elif path:
30 | self.f = open(path, 'rb')
31 | if not self.name:
32 | self.name = os.path.basename(path)
33 | elif content:
34 | self.f = BytesIO(initial_bytes=content)
35 | elif iterable:
36 | self.f = BufferedIterable(iterable)
37 | if not self.name:
38 | self.name = str(uuid.uuid4())
39 | self.headers = headers
40 | self.is_async = inspect.iscoroutine(self.f.read)
41 |
42 |
43 | class MultipartEncoder:
44 |
45 | def __init__(self, delimiter: bytes, params: dict, chunk_size: int=1*1024*1024,
46 | loop=None, encoding: str='utf-8'):
47 | self.delimiter = b'--' + delimiter
48 | self.params = params
49 | self.chunk_size = chunk_size
50 | self.evaluated = False
51 | self.loop = loop
52 | self.encoding = encoding
53 |
54 | def create_headers(self, name: str, value) -> bytes:
55 | """
56 |
57 | :param name:
58 | :param value:
59 | :return:
60 | """
61 | if isinstance(value, FileUpload):
62 | return f'Content-Disposition: form-data; name="{name}"; filename="{value.name}"'.encode(self.encoding)
63 | else:
64 | return f'Content-Disposition: form-data; name="{name}"'.encode(self.encoding)
65 |
66 | def stream_value(self, value) -> bytes:
67 | """
68 |
69 | :param value:
70 | :return:
71 | """
72 | if isinstance(value, FileUpload):
73 | while True:
74 | if value.is_async:
75 | chunk = self.loop.run_until_complete(value.f.read(self.chunk_size))
76 | else:
77 | chunk = value.f.read(self.chunk_size)
78 | size = len(chunk)
79 | if size == 0:
80 | break
81 | yield chunk
82 | else:
83 | if isinstance(value, int):
84 | yield str(value).encode()
85 | elif isinstance(value, str):
86 | yield value.encode(self.encoding)
87 | else:
88 | yield value
89 |
90 | def __iter__(self):
91 | """
92 |
93 | :return:
94 | """
95 | if self.evaluated:
96 | raise Exception('Streaming encoder cannot be evaluated twice.')
97 | for name, value in self.params.items():
98 | header = self.delimiter + b'\r\n' + self.create_headers(name, value) + b'\r\n\r\n'
99 | yield header
100 | for chunk in self.stream_value(value):
101 | yield chunk
102 | yield b'\r\n'
103 | yield self.delimiter + b'--'
104 | self.evaluated = True
105 |
--------------------------------------------------------------------------------
/vibora/multipart/parser.pxd:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class SmartFile:
4 |
5 | cdef:
6 | int in_memory_limit
7 | int pointer
8 | bint in_memory
9 | str filename
10 | str temp_dir
11 | object engine
12 | bytearray buffer
13 |
14 | cdef object consume(self)
15 |
16 |
17 | cdef class MultipartParser:
18 |
19 | cdef:
20 | bytearray data
21 | bytes end_boundary
22 | bytes start_boundary
23 | dict values
24 | str temp_dir
25 |
26 | int in_memory_threshold
27 | int boundary_length
28 | int status
29 |
30 | SmartFile current_buffer
31 |
32 | cdef inline bytearray clean_value(self, bytearray v)
33 | cdef void parse_header(self, bytearray header)
34 | cdef dict consume(self)
35 |
--------------------------------------------------------------------------------
/vibora/optimizer.py:
--------------------------------------------------------------------------------
1 | ########################################################################
2 | ########################################################################
3 | # Portions Copyright (c) 2017 Paweł Piotr Przeradowski.
4 | # https://github.com/squeaky-pl/japronto
5 | ########################################################################
6 | ########################################################################
7 | import dis
8 | from typing import Callable
9 |
10 |
11 | def is_static(route_handler: Callable) -> bool:
12 | """
13 | Checks if a given route is static.
14 | :param route_handler: The route handler (a function that produces a http response)
15 | :return: True or False
16 | """
17 | seen_load_fast_0 = False
18 | seen_return_value = False
19 | seen_call_fun = False
20 | valid_responses = ('JsonResponse', 'Response')
21 |
22 | for instruction in dis.get_instructions(route_handler):
23 |
24 | if instruction.opname == 'LOAD_GLOBAL' and instruction.argval in valid_responses:
25 | seen_load_fast_0 = True
26 | continue
27 |
28 | if instruction.opname == 'RETURN_VALUE':
29 | seen_return_value = True
30 | continue
31 |
32 | if instruction.opname.startswith('CALL_FUNCTION'):
33 | if seen_call_fun:
34 | return False
35 |
36 | seen_call_fun = True
37 | continue
38 |
39 | return seen_call_fun and seen_load_fast_0 and seen_return_value
40 |
--------------------------------------------------------------------------------
/vibora/parsers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import parser, response, errors
2 | from .parser import parse_url
3 |
4 | __all__ = parser.__all__ + errors.__all__ + response.__all__
5 |
--------------------------------------------------------------------------------
/vibora/parsers/errors.py:
--------------------------------------------------------------------------------
1 | __all__ = ('HttpParserError',
2 | 'HttpParserCallbackError',
3 | 'HttpParserInvalidStatusError',
4 | 'HttpParserInvalidMethodError',
5 | 'HttpParserInvalidURLError',
6 | 'HttpParserUpgrade',
7 | 'BodyLimitError',
8 | 'HeadersLimitError')
9 |
10 |
11 | class HttpParserError(Exception):
12 | pass
13 |
14 |
15 | class HttpParserCallbackError(HttpParserError):
16 | pass
17 |
18 |
19 | class HttpParserInvalidStatusError(HttpParserError):
20 | pass
21 |
22 |
23 | class HttpParserInvalidMethodError(HttpParserError):
24 | pass
25 |
26 |
27 | class HttpParserInvalidURLError(HttpParserError):
28 | pass
29 |
30 |
31 | class BodyLimitError(HttpParserError):
32 | pass
33 |
34 |
35 | class HeadersLimitError(HttpParserError):
36 | pass
37 |
38 |
39 | class HttpParserUpgrade(Exception):
40 | pass
41 |
--------------------------------------------------------------------------------
/vibora/parsers/parser.pxd:
--------------------------------------------------------------------------------
1 | #!python
2 | #cython: language_level=3
3 |
4 | from __future__ import print_function
5 | from cpython.mem cimport PyMem_Malloc, PyMem_Free
6 | from cpython cimport PyObject_GetBuffer, PyBuffer_Release, PyBUF_SIMPLE, \
7 | Py_buffer, PyBytes_AsString
8 |
9 | from .errors import (HttpParserError,
10 | HttpParserCallbackError,
11 | HttpParserInvalidStatusError,
12 | HttpParserInvalidMethodError,
13 | HttpParserInvalidURLError,
14 | HttpParserUpgrade)
15 | cimport cython
16 | from ..parsers cimport cparser
17 | from ..protocol.cprotocol cimport Connection
18 | from ..headers.headers cimport Headers
19 |
20 | __all__ = ('parse_url', 'HttpParser')
21 |
22 |
23 | cdef class HttpParser:
24 | cdef:
25 | cparser.http_parser* _cparser
26 | cparser.http_parser_settings* _csettings
27 |
28 | bytes _current_header_name
29 | bytes _current_header_value
30 | Connection protocol
31 |
32 | _proto_on_url, _proto_on_status, _proto_on_body, \
33 | _proto_on_header, _proto_on_headers_complete, \
34 | _proto_on_message_complete, _proto_on_chunk_header, \
35 | _proto_on_chunk_complete, _proto_on_message_begin
36 |
37 | object _last_error
38 | list _headers
39 | bytes _url
40 |
41 | Py_buffer py_buf
42 |
43 | # Security Limits
44 | int headers_limit
45 | int body_limit
46 | int max_headers_size
47 | int max_body_size
48 | int current_headers_size
49 | int current_body_size
50 |
51 | cdef _maybe_call_on_header(self)
52 | cdef _on_header_field(self, bytes field)
53 | cdef _on_header_value(self, bytes val)
54 | cdef _on_headers_complete(self)
55 | cdef _on_chunk_header(self)
56 | cdef _on_chunk_complete(self)
57 | cdef int feed_data(self, bytes data) except -1
58 |
--------------------------------------------------------------------------------
/vibora/parsers/response.pxd:
--------------------------------------------------------------------------------
1 | #!python
2 | #cython: language_level=3
3 |
4 | from . cimport cparser
5 | # noinspection PyUnresolvedReferences
6 | from cpython cimport Py_buffer
7 | # noinspection PyUnresolvedReferences
8 | from ..headers.headers cimport Headers
9 |
10 | __all__ = ('HttpResponseParser', )
11 |
12 |
13 | cdef class HttpResponseParser:
14 | cdef:
15 | cparser.http_parser_settings* csettings
16 | cparser.http_parser* cparser
17 |
18 | bytes current_header_name
19 | bytes current_header_value
20 | object protocol
21 |
22 | _proto_on_url, _proto_on_status, _proto_on_body, \
23 | _proto_on_header, _proto_on_headers_complete, \
24 | _proto_on_message_complete, _proto_on_chunk_header, \
25 | _proto_on_chunk_complete, _proto_on_message_begin
26 |
27 | object last_error
28 | Headers headers
29 | bytes current_status
30 |
31 | Py_buffer py_buf
32 |
33 | cdef _maybe_call_on_header(self)
34 | cdef on_header_field(self, bytes field)
35 | cdef on_header_value(self, bytes val)
36 | cdef on_headers_complete(self)
37 | cdef on_chunk_header(self)
38 | cdef on_chunk_complete(self)
39 | cpdef feed(self, bytes data)
40 |
--------------------------------------------------------------------------------
/vibora/parsers/typing.py:
--------------------------------------------------------------------------------
1 | class URL:
2 |
3 | def __init__(self, schema: bytes, host: bytes, port, path: bytes,
4 | query: bytes, fragment: bytes, userinfo: bytes):
5 | self.schema = schema.decode('utf-8')
6 | self.host = host.decode('utf-8')
7 | self.port = port if port else 80
8 | self.path = path.decode('utf-8')
9 | self.query = query.decode('utf-8')
10 | self.fragment = fragment.decode('utf-8')
11 | self.userinfo = userinfo.decode('utf-8')
12 | self.netloc = self.schema + '://' + self.host + self.port
13 |
14 | def __repr__(self):
15 | return (''
17 | .format(self.schema, self.host, self.port, self.path, self.query, self.fragment, self.userinfo))
18 |
--------------------------------------------------------------------------------
/vibora/protocol/__init__.py:
--------------------------------------------------------------------------------
1 | from .definitions import *
2 | from . import cprotocol
3 |
4 | locals()['Connection'] = cprotocol.Connection
5 | locals()['update_current_time'] = cprotocol.update_current_time
6 |
--------------------------------------------------------------------------------
/vibora/protocol/cprotocol.pxd:
--------------------------------------------------------------------------------
1 | #!python
2 | #cython: language_level=3, boundscheck=False, wraparound=False
3 |
4 | ###############################################
5 | # C IMPORTS
6 | # noinspection PyUnresolvedReferences
7 | from ..parsers.parser cimport HttpParser
8 | # noinspection PyUnresolvedReferences
9 | from ..router.router cimport Router, Route
10 | # noinspection PyUnresolvedReferences
11 | from ..request.request cimport Request, Stream, StreamQueue
12 | # noinspection PyUnresolvedReferences
13 | from ..headers.headers cimport Headers
14 | # noinspection PyUnresolvedReferences
15 | from ..responses.responses cimport Response, CachedResponse
16 | # noinspection PyUnresolvedReferences
17 | from ..components.components cimport ComponentsEngine
18 | ###############################################
19 |
20 | cdef class Connection:
21 | cdef:
22 | public object app
23 | int status
24 | bint keep_alive
25 | bint closed
26 | bint _stopped
27 | int write_buffer
28 | object worker
29 | public object loop
30 | public object transport
31 | public bytes protocol
32 | bint writable
33 | bint readable
34 | object write_permission
35 | HttpParser parser
36 | Router router
37 | object log
38 | Stream stream
39 | StreamQueue queue
40 | object current_task
41 | object timeout_task
42 | ComponentsEngine components
43 | int last_task_time
44 |
45 | # Caching the existence of hooks.
46 | bint before_endpoint_hooks
47 | bint after_endpoint_hooks
48 | bint after_send_response_hooks
49 | bint any_hooks
50 |
51 | object request_class
52 | object call_hooks
53 |
54 | # Asyncio Callbacks (Network Flow)
55 | cpdef void connection_made(self, transport)
56 | cpdef void data_received(self, bytes data)
57 | cpdef void connection_lost(self, exc)
58 | cpdef void pause_writing(self)
59 | cpdef void resume_writing(self)
60 |
61 | # Custom protocol methods.
62 | cdef void handle_upgrade(self)
63 | cpdef void after_response(self, Response response)
64 | cpdef void resume_reading(self)
65 | cpdef void pause_reading(self)
66 | cpdef void cancel_request(self)
67 | cpdef void close(self)
68 | cpdef void stop(self)
69 | cpdef bint is_closed(self)
70 | cpdef str client_ip(self)
71 |
72 | # Reaper related.
73 | cpdef int get_status(self)
74 | cpdef int get_last_task_time(self)
75 |
76 | # HTTP parser callbacks.
77 | cdef void on_headers_complete(self, Headers headers, bytes url, bytes method, bint upgrade)
78 | cdef void on_body(self, bytes body)
79 | cdef void on_message_complete(self)
80 |
--------------------------------------------------------------------------------
/vibora/protocol/cwebsocket.pxd:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class WebsocketConnection:
4 |
5 | cdef:
6 | object app
7 | object handler
8 | object loop
9 | object transport
10 |
11 | cpdef void data_received(self, bytes data)
12 | cpdef void connection_lost(self, exc)
13 |
--------------------------------------------------------------------------------
/vibora/protocol/cwebsocket.pyx:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class WebsocketConnection:
4 |
5 | def __cinit__(self, object app, object handler, object loop, object transport):
6 | self.app = app
7 | self.handler = handler
8 | self.loop = loop
9 | self.transport = transport
10 |
11 | cpdef void data_received(self, bytes data):
12 | task = self.handler.feed(data)
13 | self.loop.create_task(task)
14 |
15 | cpdef void connection_lost(self, exc):
16 | self.app.connections.discard(self)
17 |
--------------------------------------------------------------------------------
/vibora/protocol/definitions.py:
--------------------------------------------------------------------------------
1 | from asyncio.transports import Transport
2 |
3 |
4 | class ConnectionStatus:
5 | PENDING = 1
6 | PROCESSING_REQUEST = 2
7 | WEBSOCKET = 3
8 |
9 |
10 | class Connection:
11 |
12 | def __init__(self, app, loop): pass
13 |
14 | def connection_made(self, transport: Transport): pass
15 |
16 | def data_received(self, data): pass
17 |
18 | def write_response(self, response): pass
19 |
20 | async def call_async_hooks(self, type_id: int, **kwargs) -> bool:
21 | pass
22 |
23 | async def process_async_request(self, route, request, stream):
24 | pass
25 |
26 | def connection_lost(self, exc): pass
27 |
28 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
29 | # HTTP PARSER CALLBACKS
30 |
31 | def on_headers_complete(self, headers: dict, url: bytes, method: bytes): pass
32 |
33 | def on_body(self, body): pass
34 |
35 | def on_message_complete(self): pass
36 |
37 |
38 | def update_current_time() -> None:
39 | pass
40 |
--------------------------------------------------------------------------------
/vibora/request/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 | from .request import *
3 | if TYPE_CHECKING:
4 | from .hints import *
5 |
--------------------------------------------------------------------------------
/vibora/request/hints.py:
--------------------------------------------------------------------------------
1 | """
2 | |===== Warning ================================================================================|
3 | | This is a stub file to provide type hints because this module is fully implemented in Cython |
4 | |==============================================================================================|
5 | """
6 | from ..headers import Headers
7 | from ..multipart import UploadedFile
8 | from ..sessions import Session
9 | from typing import List, Callable
10 |
11 |
12 | class Request:
13 |
14 | def __init__(self, url: bytes, headers: Headers, method: bytes, stream, protocol):
15 | self.url = url
16 | self.headers = headers
17 | self.method = method
18 | self.stream = stream
19 | self.protocol = protocol
20 | self.cookies: dict = {}
21 | self.args: dict = {}
22 | self.context: dict = {}
23 |
24 | def client_ip(self) -> str:
25 | """
26 |
27 | :return:
28 | """
29 | pass
30 |
31 | def session_pending_flush(self) -> bool:
32 | """
33 |
34 | :return:
35 | """
36 | pass
37 |
38 | async def form(self) -> dict:
39 | """
40 |
41 | :return:
42 | """
43 | pass
44 |
45 | async def files(self) -> List[UploadedFile]:
46 | """
47 |
48 | :return:
49 | """
50 | pass
51 |
52 | async def _load_form(self) -> None:
53 | """
54 |
55 | :return:
56 | """
57 | pass
58 |
59 | async def json(self, loads: Callable=None, strict: bool = False) -> dict:
60 | """
61 |
62 | :param loads:
63 | :param strict:
64 | :return:
65 | """
66 | pass
67 |
68 | async def session(self) -> Session:
69 | """
70 |
71 | :return:
72 | """
73 | pass
74 |
--------------------------------------------------------------------------------
/vibora/request/request.pxd:
--------------------------------------------------------------------------------
1 | # cython: language_level=3, boundscheck=False, wraparound=False, annotation_typing=False
2 | import cython
3 | # noinspection PyUnresolvedReferences
4 | from ..headers.headers cimport Headers
5 | # noinspection PyUnresolvedReferences
6 | from ..protocol.cprotocol cimport Connection
7 |
8 |
9 | cdef class StreamQueue:
10 |
11 | cdef:
12 | readonly object items
13 | object event
14 | bint waiting
15 | bint dirty
16 | bint finished
17 |
18 | cdef void put(self, bytes item)
19 | cdef void clear(self)
20 | cdef void end(self)
21 |
22 |
23 | cdef class Stream:
24 |
25 | cdef:
26 | bint consumed
27 | StreamQueue queue
28 | Connection connection
29 |
30 | cdef void clear(self)
31 |
32 |
33 | @cython.freelist(409600)
34 | cdef class Request:
35 |
36 | cdef:
37 | readonly bytes url
38 | readonly bytes method
39 | readonly object parent
40 | readonly Connection protocol
41 | readonly Headers headers
42 | readonly Stream stream
43 | readonly dict context
44 | object _cookies
45 | object _parsed_url
46 | object _args
47 | object _session
48 | dict _form
49 |
50 | cpdef str client_ip(self)
51 | cpdef session_pending_flush(self)
52 |
--------------------------------------------------------------------------------
/vibora/responses/__init__.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 | from .responses import *
3 | if TYPE_CHECKING:
4 | from .hints import *
5 |
--------------------------------------------------------------------------------
/vibora/responses/hints.py:
--------------------------------------------------------------------------------
1 | """
2 | |===== Warning ================================================================================|
3 | | This is a stub file to provide type hints because this module is fully implemented in Cython |
4 | |==============================================================================================|
5 | """
6 | from typing import Callable
7 | from inspect import isasyncgenfunction
8 | from ..utils import json
9 |
10 |
11 | class Response:
12 |
13 | def __init__(self, content: bytes, status_code: int = 200, headers: dict = None, cookies: list = None):
14 | self.status_code: int = status_code
15 | self.content: bytes = content
16 | self.headers: dict = headers or {}
17 | self.cookies: list = cookies or []
18 |
19 |
20 | class CachedResponse(Response):
21 |
22 | def __init__(self, content: bytes, status_code: int = 200, headers: dict = None, cookies: list = None):
23 | super().__init__(content=content, status_code=status_code, headers=headers, cookies=cookies)
24 | self.content = content
25 | self.cache = None
26 |
27 |
28 | class JsonResponse(Response):
29 |
30 | def __init__(self, content: object, status_code: int = 200, headers: dict = None, cookies: list = None):
31 | super().__init__(content=json.dumps(content).encode(), status_code=status_code,
32 | headers=headers, cookies=cookies)
33 |
34 |
35 | class RedirectResponse(Response):
36 |
37 | def __init__(self, location: str, status_code: int = 302, headers: dict = None, cookies: list = None):
38 | super().__init__(b'', status_code=status_code, headers=headers, cookies=cookies)
39 |
40 |
41 | class StreamingResponse(Response):
42 |
43 | def __init__(self, stream: Callable, status_code: int = 200, headers: dict = None, cookies: list = None,
44 | complete_timeout: int = 30, chunk_timeout: int = 10):
45 | super().__init__(b'', status_code=status_code, headers=headers, cookies=cookies)
46 | if not callable(stream):
47 | raise ValueError('StreamingResponse "stream" must be a callable.')
48 | self.stream = stream
49 | self.content = b''
50 | self.is_async = isasyncgenfunction(stream)
51 | if 'Content-Length' in self.headers:
52 | self.chunked = False
53 | else:
54 | self.chunked = True
55 | self.headers['Transfer-Encoding'] = 'chunked'
56 | self.complete_timeout = complete_timeout
57 | self.chunk_timeout = chunk_timeout
58 |
--------------------------------------------------------------------------------
/vibora/responses/responses.pxd:
--------------------------------------------------------------------------------
1 | #cython: language_level=3, boundscheck=False, wraparound=False
2 | import cython
3 |
4 | # C IMPORTS
5 | # noinspection PyUnresolvedReferences
6 | from ..protocol.cprotocol cimport Connection
7 | ###############################################
8 |
9 | cdef str current_time
10 |
11 |
12 | @cython.freelist(409600)
13 | cdef class Response:
14 |
15 | cdef:
16 | public int status_code
17 | public bytes content
18 | public dict headers
19 | public list cookies
20 | public bint skip_hooks
21 |
22 | cdef bytes encode(self)
23 |
24 | cdef void send(self, Connection protocol)
25 |
26 |
27 | cdef class CachedResponse(Response):
28 | cdef tuple cache
29 | cdef void send(self, Connection protocol)
30 |
31 |
32 | cdef class JsonResponse(Response):
33 | pass
34 |
35 |
36 | cdef class WebsocketHandshakeResponse(Response):
37 | pass
38 |
39 |
40 | cdef class RedirectResponse(Response):
41 | pass
42 |
43 |
44 | cdef class StreamingResponse(Response):
45 | cdef:
46 | public bytes content_type
47 | public object stream
48 | public bint is_async
49 | public int complete_timeout
50 | public int chunk_timeout
51 | bint chunked
52 |
53 | cdef bytes encode(self)
54 |
55 | cdef void send(self, Connection protocol)
56 |
--------------------------------------------------------------------------------
/vibora/router/__init__.py:
--------------------------------------------------------------------------------
1 | from .router import *
2 |
--------------------------------------------------------------------------------
/vibora/router/parser.py:
--------------------------------------------------------------------------------
1 | import re
2 | from ..exceptions import RouteConfigurationError
3 |
4 |
5 | class PatternParser:
6 |
7 | PARAM_REGEX = re.compile(b'<.*?>')
8 | DYNAMIC_CHARS = bytearray(b'*?.[]()')
9 |
10 | CAST = {
11 | str: lambda x: x.decode('utf-8'),
12 | int: lambda x: int(x),
13 | float: lambda x: float(x)
14 | }
15 |
16 | @classmethod
17 | def validate_param_name(cls, name: bytes):
18 | # TODO:
19 | if b':' in name:
20 | raise RouteConfigurationError('Special characters are not allowed in param name. '
21 | 'Use type hints in function parameters to cast the variable '
22 | 'or regexes with named groups to ensure only a specific URL matches.')
23 |
24 | @classmethod
25 | def extract_params(cls, pattern: bytes) -> tuple:
26 | """
27 |
28 | :param pattern:
29 | :return:
30 | """
31 | params = []
32 | new_pattern = pattern
33 | simplified_pattern = pattern
34 | groups = cls.PARAM_REGEX.findall(pattern)
35 | for group in groups:
36 | name = group[1:-1] # Removing <> chars
37 | cls.validate_param_name(name)
38 | simplified_pattern = simplified_pattern.replace(group, b'$' + name)
39 | params.append(name.decode())
40 | new_pattern = new_pattern.replace(group, b'(?P<' + name + b'>[^/]+)')
41 | return re.compile(new_pattern), params, simplified_pattern
42 |
43 | @classmethod
44 | def is_dynamic_pattern(cls, pattern: bytes) -> bool:
45 | for index, char in enumerate(pattern):
46 | if char in cls.DYNAMIC_CHARS:
47 | if index > 0 and pattern[index - 1] == '\\':
48 | continue
49 | return True
50 | return False
51 |
--------------------------------------------------------------------------------
/vibora/router/router.pxd:
--------------------------------------------------------------------------------
1 | # cython: language_level=3, boundscheck=False, wraparound=False, annotation_typing=False
2 | import cython
3 |
4 | ############################################
5 | # C IMPORTS
6 | # noinspection PyUnresolvedReferences
7 | from ..request.request cimport Request
8 | # noinspection PyUnresolvedReferences
9 | from ..cache.cache cimport CacheEngine
10 | # noinspection PyUnresolvedReferences
11 | from ..responses.responses cimport Response, RedirectResponse, WebsocketHandshakeResponse
12 | # noinspection PyUnresolvedReferences
13 | from ..components.components cimport ComponentsEngine
14 | ############################################
15 |
16 |
17 | cdef class LRUCache:
18 | cdef:
19 | dict values
20 | object queue
21 | int max_size
22 | int current_size
23 |
24 | cdef set(self, tuple key, Route route)
25 |
26 |
27 | cdef class Route:
28 |
29 | cdef:
30 | public str name
31 | public object handler
32 | public object app
33 | public object parent
34 | public bytes pattern
35 | public tuple components
36 | public bint receive_params
37 | public bint is_coroutine
38 | readonly tuple methods
39 | public object websocket
40 | public object regex
41 | public list params_book
42 | public object simplified_pattern
43 | public bint has_parameters
44 | public list hosts
45 | public bint is_dynamic
46 | CacheEngine cache
47 | public object limits
48 |
49 | cdef inline object call_handler(self, Request request, ComponentsEngine components)
50 |
51 |
52 | cdef class Router:
53 | cdef:
54 | int strategy
55 | readonly dict reverse_index
56 | dict routes
57 | dict dynamic_routes
58 | public dict default_handlers
59 | LRUCache cache
60 | dict hosts
61 | bint check_host
62 |
63 | cdef bint check_not_allowed_method(self, bytes url, bytes method) except -1
64 |
65 | cdef Route get_route(self, Request request)
66 |
67 | @cython.locals(key=tuple, route=Route)
68 | cdef Route _find_route(self, bytes url, bytes method)
69 |
70 | @cython.locals(key=tuple, route=Route)
71 | cdef Route _find_route_by_host(self, bytes url, bytes method, str host)
72 |
--------------------------------------------------------------------------------
/vibora/schemas/__init__.py:
--------------------------------------------------------------------------------
1 | from .extensions import fields
2 | from .schemas import *
3 | from .extensions.schemas import Schema as CythonSchema
4 |
5 |
6 | locals()['Schema'] = CythonSchema
7 |
--------------------------------------------------------------------------------
/vibora/schemas/exceptions.py:
--------------------------------------------------------------------------------
1 | from vibora.utils import json
2 |
3 |
4 | class ValidationError(Exception):
5 | def __init__(self, msg=None, field=None, error_code: int=0, **extra):
6 | self.msg = msg
7 | self.field = field
8 | self.extra = extra
9 | self.error_code = error_code
10 | super().__init__(str(msg))
11 |
12 |
13 | class NestedValidationError(Exception):
14 | def __init__(self, context):
15 | self.context = context
16 | super().__init__()
17 |
18 |
19 | class InvalidSchema(Exception):
20 | def __init__(self, errors: dict):
21 | self.errors = errors
22 | super().__init__('Invalid schema: ' + json.dumps(errors))
23 |
--------------------------------------------------------------------------------
/vibora/schemas/extensions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/vibora/schemas/extensions/__init__.py
--------------------------------------------------------------------------------
/vibora/schemas/extensions/fields.pxd:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class Field:
4 | cdef:
5 | readonly list validators
6 | public bint strict
7 | public bint is_async
8 | public str load_from
9 | public str load_into
10 | object default
11 | public bint required
12 | bint default_callable
13 |
14 |
15 | cdef load(self, value)
16 | cdef sync_pipeline(self, object value, dict context)
17 | cdef _call_sync_validators(self, object value, dict context)
18 |
19 |
20 | cdef class Integer(Field):
21 | pass
22 |
23 |
24 | cdef class Number(Field):
25 | pass
26 |
27 |
28 | cdef class String(Field):
29 | pass
30 |
31 |
32 | cdef class List(Field):
33 | pass
34 |
35 |
36 | cdef class Nested(Field):
37 | pass
--------------------------------------------------------------------------------
/vibora/schemas/extensions/validator.pxd:
--------------------------------------------------------------------------------
1 |
2 |
3 | cdef class Validator:
4 | cdef:
5 | object f
6 | bint is_async
7 | int params_count
8 |
9 | cdef validate(self, object value, dict context)
10 |
--------------------------------------------------------------------------------
/vibora/schemas/extensions/validator.pyx:
--------------------------------------------------------------------------------
1 | from inspect import signature, iscoroutinefunction
2 | from typing import Callable
3 |
4 |
5 | cdef class Validator:
6 |
7 | def __init__(self, f: Callable=None):
8 | self.f = f
9 | self.is_async = iscoroutinefunction(f)
10 | try:
11 | self.params_count = len(signature(f).parameters) if f else 0
12 | except TypeError:
13 | self.params_count = len(signature(f.__func__).parameters) if f else 0
14 |
15 | cdef validate(self, value, dict context):
16 | if self.params_count == 1:
17 | return self.f(value)
18 | elif self.params_count == 2:
19 | return self.f(value, context)
20 |
--------------------------------------------------------------------------------
/vibora/schemas/messages.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class Messages:
4 | MISSING_REQUIRED_FIELD = 1
5 | MUST_BE_STRING = 2
6 | MUST_BE_INTEGER = 3
7 | MUST_BE_NUMBER = 4
8 | MUST_BE_LIST = 5
9 | MUST_BE_DICT = 6
10 | MINIMUM_LENGTH = 7
11 | MAXIMUM_LENGTH = 8
12 |
13 |
14 | EnglishLanguage = {
15 | Messages.MISSING_REQUIRED_FIELD: 'Missing required field',
16 | Messages.MUST_BE_STRING: 'Must be a string',
17 | Messages.MUST_BE_INTEGER: 'Must be a valid integer',
18 | Messages.MUST_BE_NUMBER: 'Must be a valid number',
19 | Messages.MUST_BE_LIST: 'Must be a list',
20 | Messages.MUST_BE_DICT: 'Must be a map',
21 | Messages.MINIMUM_LENGTH: 'At least {minimum_value} character(s).',
22 | Messages.MAXIMUM_LENGTH: 'Maximum of {maximum_value} of character(s).'
23 | }
24 |
--------------------------------------------------------------------------------
/vibora/schemas/schemas.py:
--------------------------------------------------------------------------------
1 | from .messages import EnglishLanguage
2 | from ..request import Request
3 |
4 |
5 | class Schema:
6 |
7 | _fields = []
8 |
9 | def __init__(self, silent: bool=False):
10 | """
11 |
12 | :param silent:
13 | """
14 | pass
15 |
16 | @classmethod
17 | async def load(cls, values: dict, language: dict=EnglishLanguage, context: dict=None) -> 'Schema':
18 | """
19 |
20 | :param context:
21 | :param values:
22 | :param language:
23 | :return:
24 | """
25 | pass
26 |
27 | @classmethod
28 | async def load_form(cls, request: Request, language: dict=EnglishLanguage, context: dict=None) -> 'Schema':
29 | """
30 |
31 | :param context:
32 | :param request:
33 | :param language:
34 | :return:
35 | """
36 | pass
37 |
38 | @classmethod
39 | async def load_json(cls, request: Request, language: dict = EnglishLanguage, context: dict=None) -> 'Schema':
40 | """
41 |
42 | :param context:
43 | :param request:
44 | :param language:
45 | :return:
46 | """
47 | pass
48 |
--------------------------------------------------------------------------------
/vibora/schemas/types.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class Email(str):
4 | pass
5 |
--------------------------------------------------------------------------------
/vibora/schemas/validators.py:
--------------------------------------------------------------------------------
1 | from .exceptions import ValidationError
2 | from .messages import Messages
3 | import math
4 |
5 |
6 | class Length:
7 | def __init__(self, min: int=0, max: int=math.inf):
8 | self.min = min
9 | self.max = max
10 |
11 | def __call__(self, x):
12 | size = len(x)
13 | if size < self.min:
14 | raise ValidationError(error_code=Messages.MINIMUM_LENGTH, minimum_value=self.min)
15 | if size > self.max:
16 | raise ValidationError(error_code=Messages.MAXIMUM_LENGTH, maximum_value=self.max)
17 |
--------------------------------------------------------------------------------
/vibora/sessions/__init__.py:
--------------------------------------------------------------------------------
1 | from .base import *
2 |
--------------------------------------------------------------------------------
/vibora/sessions/base.py:
--------------------------------------------------------------------------------
1 | from ..utils import json
2 |
3 |
4 | class SessionEngine:
5 |
6 | def __init__(self, cookie_name: str=None):
7 | self.cookie_name = cookie_name or 'SESSION_ID'
8 |
9 | async def load(self, request):
10 | raise NotImplementedError
11 |
12 | async def save(self, request, response):
13 | raise NotImplementedError
14 |
15 | async def clean_up(self):
16 | pass
17 |
18 |
19 | class Session:
20 | __slots__ = ('values', 'pending_flush', 'uuid')
21 |
22 | def __init__(self, values: dict = None, pending_flush: bool=False, unique_id: str=None):
23 | self.uuid = unique_id
24 | self.values = values or {}
25 | self.pending_flush = pending_flush
26 |
27 | def __setitem__(self, key, value):
28 | self.values[key] = value
29 | self.pending_flush = True
30 |
31 | def __getitem__(self, item):
32 | return self.values[item]
33 |
34 | def get(self, item, default=None):
35 | try:
36 | return self.values[item]
37 | except KeyError:
38 | return default
39 |
40 | def __delitem__(self, key):
41 | del self.values[key]
42 | self.pending_flush = True
43 |
44 | def dump(self):
45 | return self.values
46 |
47 | def dumps(self):
48 | return json.dumps(self.values)
49 |
50 | def load(self, values: dict):
51 | self.values.update(values)
52 | self.pending_flush = True
53 |
54 | def clear(self):
55 | self.values.clear()
56 | self.pending_flush = True
57 |
58 | def __contains__(self, item):
59 | return item in self.values
60 |
--------------------------------------------------------------------------------
/vibora/sessions/client.py:
--------------------------------------------------------------------------------
1 | import ujson
2 | from typing import Optional
3 | from .base import SessionEngine, Session
4 | try:
5 | from cryptography.fernet import Fernet
6 | except ImportError:
7 | raise ImportError('To use encrypted sessions you need to install the cryptography library or implement your own '
8 | 'engine by extending the SessionEngine class.')
9 |
10 |
11 | class EncryptedCookiesEngine(SessionEngine):
12 | def __init__(self, cookie_name='vibora', secret_key=None):
13 | super().__init__(cookie_name=cookie_name)
14 | self.cipher = Fernet(secret_key)
15 |
16 | def load_cookie(self, request) -> Optional[str]:
17 | """
18 |
19 | :param request:
20 | :return:
21 | """
22 | cookie = request.cookies.get(self.cookie_name)
23 | if cookie:
24 | try:
25 | return self.cipher.decrypt(cookie.encode())
26 | except (AttributeError, ValueError):
27 | pass
28 |
29 | async def load(self, request) -> Session:
30 | """
31 |
32 | :param request:
33 | :return:
34 | """
35 | cookie = self.load_cookie(request)
36 | if cookie:
37 | try:
38 | return Session(ujson.loads(cookie))
39 | except ValueError:
40 | # In this case, the user has an invalid session.
41 | # Probably a malicious user or secret key update.
42 | return Session(pending_flush=True)
43 | return Session()
44 |
45 | async def save(self, request, response) -> None:
46 | """
47 | Inject headers in the response object so the user will receive
48 | an encrypted cookie with session values.
49 | :param request: current Request object
50 | :param response: current Response object where headers will be inject.
51 | :return:
52 | """
53 | value = self.cipher.encrypt(request.session.dumps().encode())
54 | cookie = f'{self.cookie_name}={value.decode()}; SameSite=Lax'
55 | response.headers['Set-Cookie'] = cookie
56 |
--------------------------------------------------------------------------------
/vibora/sessions/files.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | import os
3 | from .base import Session, SessionEngine
4 | from ..utils import json
5 |
6 |
7 | class FilesSessionEngine(SessionEngine):
8 |
9 | def __init__(self, storage_path: str, cookie_name: str='SESSION_ID'):
10 | super().__init__(cookie_name=cookie_name)
11 | self.storage_path = storage_path
12 | try:
13 | os.mkdir(self.storage_path)
14 | except FileExistsError:
15 | pass
16 | self.cookie_name = cookie_name
17 |
18 | async def load(self, cookies) -> Session:
19 | """
20 |
21 | :param cookies:
22 | :return:
23 | """
24 | session_id = cookies.get(self.cookie_name)
25 | if session_id:
26 | try:
27 | with open(os.path.join(self.storage_path, session_id)) as f:
28 | values = json.loads(f.read())
29 | return Session(values, unique_id=session_id)
30 | except FileNotFoundError:
31 | pass
32 | return Session(unique_id=str(uuid.uuid4()))
33 |
34 | async def save(self, session: Session, response):
35 | """
36 |
37 | :param session:
38 | :param response:
39 | :return:
40 | """
41 | with open(os.path.join(self.storage_path, session.uuid), 'w') as f:
42 | f.write(session.dumps())
43 | cookie = f'{self.cookie_name}={session.uuid}; SameSite=Lax'
44 | response.headers['Set-Cookie'] = cookie
45 |
--------------------------------------------------------------------------------
/vibora/templates/__init__.py:
--------------------------------------------------------------------------------
1 | from .template import *
2 | from .engine import *
3 |
--------------------------------------------------------------------------------
/vibora/templates/ast.py:
--------------------------------------------------------------------------------
1 | from collections import Callable
2 | from .nodes import BlockNode, MacroNode, IncludeNode, Node
3 |
4 |
5 | def find_all(search_function, node):
6 | elements = []
7 | for child in node.children:
8 | if search_function(child):
9 | elements.append(child)
10 | if child.children:
11 | find_all(search_function, child)
12 | return elements
13 |
14 |
15 | def replace_on_tree(look_for: Callable, new_node: Callable, current_node: Node):
16 | """
17 |
18 | :param new_node:
19 | :param look_for:
20 | :param current_node:
21 | :return:
22 | """
23 | pending_replace = []
24 | for index, child in enumerate(current_node.children):
25 | if look_for(child):
26 | pending_replace.append(index)
27 | for index in pending_replace:
28 | current_node.children[index] = new_node(current_node.children[index])
29 | for index, child in enumerate(current_node.children):
30 | if index not in pending_replace:
31 | replace_on_tree(look_for, new_node, child)
32 |
33 |
34 | def replace_all(search_function, replace_with, node):
35 | new_children = []
36 | for child in node.children:
37 | if search_function(child):
38 | new_children.append(replace_with)
39 | else:
40 | new_children.append(child)
41 | if child.children:
42 | replace_all(search_function, replace_with, child)
43 | node.children = new_children
44 |
45 |
46 | def raise_nodes(match, node, inner=False):
47 | nodes_to_be_raised = []
48 | nodes_to_be_deleted = []
49 | for index, child in enumerate(node.children):
50 | if match(child):
51 | nodes_to_be_raised.append(child)
52 | nodes_to_be_deleted.append(index)
53 | if child.children:
54 | for inner_child in child.children:
55 | nodes = raise_nodes(match, inner_child, inner=True)
56 | nodes_to_be_raised += nodes
57 | for index in sorted(nodes_to_be_deleted, reverse=True):
58 | del node.children[index]
59 | if inner is False:
60 | node.children = nodes_to_be_raised + node.children
61 | else:
62 | return nodes_to_be_raised
63 |
64 |
65 | def merge(a, b):
66 | new_ast = a.ast
67 |
68 | # Replacing blocks.
69 | new_blocks = find_all(lambda x: isinstance(x, BlockNode), b.ast)
70 | for block in new_blocks:
71 | replace_all(lambda x: isinstance(x, BlockNode) and x.name == block.name, block, new_ast)
72 |
73 | # Preserving macros.
74 | macros = find_all(lambda x: isinstance(x, MacroNode), b.ast)
75 | for macro in macros:
76 | a.ast.children.append(macro)
77 |
78 | return new_ast
79 |
80 |
81 | def resolve_include_nodes(engine, nodes: list) -> list:
82 | relationships = []
83 | for index, node in enumerate(nodes):
84 | if isinstance(node, IncludeNode):
85 | target = engine.get_template(node.target)
86 | relationships.append(target)
87 | engine.prepare_template(target)
88 | nodes[index] = target.ast
89 | elif node.children:
90 | inner_relationships = resolve_include_nodes(engine, node.children)
91 | relationships += inner_relationships
92 | return relationships
93 |
--------------------------------------------------------------------------------
/vibora/templates/compilers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/vibora/templates/compilers/__init__.py
--------------------------------------------------------------------------------
/vibora/templates/compilers/base.py:
--------------------------------------------------------------------------------
1 | from ..utils import TemplateMeta, CompilationResult
2 |
3 |
4 | class TemplateCompiler:
5 |
6 | NAME = 'compiler'
7 | VERSION = '0.0.0'
8 |
9 | def __init__(self):
10 | self._indentation = 0
11 | self.pending_statement = False
12 |
13 | def clean(self):
14 | raise NotImplementedError
15 |
16 | def indent(self):
17 | self._indentation += 4
18 | self.pending_statement = True
19 |
20 | def rollback(self):
21 | self.flush_text()
22 | if self.pending_statement and self._indentation > 4:
23 | self.add_statement('pass')
24 | elif self.pending_statement and self._indentation == 4:
25 | # This is a special case when the template is actually empty.
26 | self.add_statement("yield ''")
27 | self._indentation -= 4
28 |
29 | def add_text(self, content: str):
30 | raise NotImplementedError
31 |
32 | def flush_text(self):
33 | raise NotImplementedError
34 |
35 | def add_statement(self, content: str):
36 | raise NotImplementedError
37 |
38 | def consume(self, template):
39 | raise NotImplementedError
40 |
41 | @classmethod
42 | def create_new_macro(cls, definition: str):
43 | raise NotImplementedError
44 |
45 | @classmethod
46 | def load_compiled_template(cls, meta: TemplateMeta, content: bytes):
47 | raise NotImplementedError
48 |
49 | def compile(self, template, verbose: bool=False) -> CompilationResult:
50 | raise NotImplementedError
51 |
52 | @classmethod
53 | def generate_template_name(cls, hash_: str):
54 | return hash_
55 |
--------------------------------------------------------------------------------
/vibora/templates/compilers/helpers.py:
--------------------------------------------------------------------------------
1 | from inspect import isasyncgen, isasyncgenfunction, iscoroutine
2 |
3 |
4 | async def smart_iter(element):
5 | if isasyncgen(element) or isasyncgenfunction(element):
6 | async for x in element:
7 | yield x
8 | else:
9 | for x in element:
10 | yield x
11 |
--------------------------------------------------------------------------------
/vibora/templates/exceptions.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 |
4 | class TemplateError(Exception):
5 | pass
6 |
7 |
8 | class TemplateRenderError(TemplateError):
9 | def __init__(self, template, template_line, exception, template_name):
10 | self.template = template
11 | self.original_exception = exception
12 | self.template_line = template_line
13 | super().__init__(json.dumps({'template_line': template_line, 'error': str(exception),
14 | 'template_name': template_name}))
15 |
16 |
17 | class DuplicatedTemplateName(TemplateError):
18 | pass
19 |
20 |
21 | class MissingEnvironment(TemplateError):
22 | pass
23 |
24 |
25 | class TemplateNotFound(TemplateError):
26 | pass
27 |
28 |
29 | class InvalidTag(TemplateError):
30 | pass
31 |
32 |
33 | class InvalidExpression(TemplateError):
34 | pass
35 |
36 |
37 | class FailedToCompileTemplate(TemplateError):
38 | pass
39 |
40 |
41 | class ConflictingNames(TemplateError):
42 | pass
43 |
44 |
45 | class InvalidVersion(TemplateError):
46 | pass
47 |
48 |
49 | class InvalidArchitecture(TemplateError):
50 | pass
51 |
--------------------------------------------------------------------------------
/vibora/templates/extensions.py:
--------------------------------------------------------------------------------
1 | from .engine import TemplateEngine, Template
2 | from .nodes import UrlNode, TextNode, StaticNode
3 | from .ast import replace_on_tree
4 |
5 |
6 | class EngineExtension:
7 |
8 | def before_compile(self, engine: TemplateEngine, template: Template):
9 | pass
10 |
11 |
12 | class ViboraNodes(EngineExtension):
13 |
14 | def __init__(self, app):
15 | super().__init__()
16 | self.app = app
17 |
18 | def before_prepare(self, engine: TemplateEngine, template: Template):
19 | """
20 | Handling specific Vibora nodes.
21 | The template engine is not tied to Vibora, the integration is done through extensions.
22 | :param engine:
23 | :param template:
24 | :return:
25 | """
26 | replace_on_tree(lambda node: isinstance(node, UrlNode),
27 | lambda node: TextNode(self.app.url_for(node.url)),
28 | current_node=template.ast)
29 |
30 | def replace_static(node):
31 | if not self.app.static:
32 | msg = 'Please configure a static handler before using a {% static %} tag in your templates.'
33 | raise NotImplementedError(msg)
34 | url = self.app.static.url_for(node.url)
35 | return TextNode(url)
36 |
37 | replace_on_tree(lambda x: isinstance(x, StaticNode), replace_static, current_node=template.ast)
38 |
--------------------------------------------------------------------------------
/vibora/templates/loader.py:
--------------------------------------------------------------------------------
1 | import time
2 | import os
3 | import threading
4 | from .engine import TemplateEngine
5 | from .template import Template
6 | from .utils import get_import_names
7 |
8 |
9 | class TemplateLoader(threading.Thread):
10 | def __init__(self, directories: list, engine: TemplateEngine, supported_files: list=None, interval: int=0.5):
11 | super().__init__()
12 | self.directories = directories
13 | self.engine = engine
14 | self.supported_files = supported_files or ('.html', '.vib')
15 | self.cache = {}
16 | self.path_index = {}
17 | self.hash_index = {}
18 | self.interval = interval
19 | self.has_to_run = True
20 |
21 | def reload_templates(self, paths: list):
22 | for root, path in paths:
23 | if path in self.path_index:
24 | template = self.path_index[path]
25 |
26 | # Searching for templates who depends on this one.
27 | relationships = []
28 | for meta in self.engine.cache.loaded_metas.values():
29 | if template.hash in meta.dependencies:
30 | relationships.append(meta.template_hash)
31 |
32 | # In case we found dependencies we need to reload them too.
33 | for template_hash in relationships:
34 | if template_hash in self.hash_index:
35 | values = self.hash_index[template_hash]
36 | self.engine.remove_template(values[2])
37 | self.add_to_engine(values[0], values[1])
38 |
39 | # Removing the actual template.
40 | self.engine.remove_template(template)
41 |
42 | self.add_to_engine(root, path)
43 | self.engine.sync_cache()
44 | self.engine.compile_templates()
45 |
46 | def check_for_modified_templates(self):
47 | to_be_notified = []
48 | for path in self.directories:
49 | for root, dirs, files in os.walk(path):
50 | for file in [f for f in files if f.endswith(self.supported_files)]:
51 | path = os.path.join(root, file)
52 | try:
53 | last_modified = os.path.getmtime(path)
54 | if path in self.cache:
55 | if self.cache[path] != last_modified:
56 | to_be_notified.append((root, path))
57 | else:
58 | to_be_notified.append((root, path))
59 | self.cache[path] = last_modified
60 | except FileNotFoundError:
61 | continue
62 | if to_be_notified:
63 | self.reload_templates(to_be_notified)
64 |
65 | def add_to_engine(self, root: str, path: str):
66 | with open(path, 'r') as f:
67 | template = Template(f.read())
68 | names = get_import_names(root, path)
69 | template = self.engine.add_template(template, names=names)
70 | self.path_index[path] = template
71 | self.hash_index[template.hash] = (root, path, template)
72 |
73 | def load(self):
74 | for directory in self.directories:
75 | for root, dirs, files in os.walk(directory):
76 | for file in files:
77 | if file.endswith(self.supported_files):
78 | path = os.path.join(root, file)
79 | self.add_to_engine(root, path)
80 |
81 | def run(self):
82 | while self.has_to_run:
83 | self.check_for_modified_templates()
84 | time.sleep(self.interval)
85 |
--------------------------------------------------------------------------------
/vibora/templates/parser.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | parser = re.compile('[a-z_]+')
4 | whitelist = ['or', 'and', 'range', 'int', 'str']
5 |
6 |
7 | def prepare_expression(expression: str, context_var: str, scope: list):
8 | e = expression
9 | handicap = 0
10 | for match in parser.finditer(expression):
11 | start, end = match.start(), match.end()
12 | if match.group() not in scope and match.group() not in whitelist:
13 | conditions = (
14 | expression[(start - 1)] not in ('"', "'", ".") if start > 0 else True,
15 | expression[end] != '=' if (end - 1) > len(expression) else True
16 | )
17 | if all(conditions):
18 | replace_for = context_var + '.get("' + match.group() + '")'
19 | e = e[:match.start() + handicap] + replace_for + e[match.end() + handicap:]
20 | handicap += len(replace_for) - len(match.group())
21 | return e
22 |
--------------------------------------------------------------------------------
/vibora/templates/utils.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import platform
4 | import typing
5 | from .exceptions import FailedToCompileTemplate
6 |
7 |
8 | def find_template_binary(path: str):
9 | name = os.listdir(path)[-1]
10 | if not name:
11 | raise FailedToCompileTemplate(path)
12 | return os.path.join(path, name)
13 |
14 |
15 | class CompilerFlavor:
16 | TEMPLATE = 1
17 | MACRO = 2
18 |
19 |
20 | def get_scope_by_args(function_def: str):
21 | open_at = function_def.find('(') + 1
22 | close_at = function_def.rfind(')')
23 | args = function_def[open_at:close_at]
24 | scope = []
25 | for value in args.split(','):
26 | if value.find('='):
27 | scope.append(value.split('=')[0].strip())
28 | else:
29 | scope.append(value.strip())
30 | return scope
31 |
32 |
33 | def get_function_name(definition: str):
34 | return definition[:definition.find('(')].strip()
35 |
36 |
37 | class TemplateMeta:
38 | def __init__(self, entry_point: str, version: str, template_hash: str,
39 | created_at: str, compiler: str, architecture: str, compilation_time: float,
40 | dependencies: list=None):
41 | self.entry_point = entry_point
42 | self.version = version
43 | self.template_hash = template_hash
44 | self.created_at = created_at
45 | self.compiler = compiler
46 | self.architecture = architecture
47 | self.compilation_time = compilation_time
48 | self.dependencies = dependencies or []
49 |
50 | @classmethod
51 | def load_from_path(cls, path: str):
52 | with open(path) as f:
53 | return TemplateMeta(**json.loads(f.read()))
54 |
55 | def store(self, path: str):
56 | with open(path, 'w') as f:
57 | values = self.__dict__.copy()
58 | values['dependencies'] = list(self.dependencies)
59 | f.write(json.dumps(values))
60 |
61 |
62 | class CompilationResult:
63 | def __init__(self, template, meta: TemplateMeta, render_function: typing.Callable,
64 | code: bytes):
65 | self.template = template
66 | self.meta = meta
67 | self.render_function = render_function
68 | self.code = code
69 |
70 |
71 | def get_architecture_signature() -> str:
72 | return ''.join(platform.architecture())
73 |
74 |
75 | def generate_entry_point(template) -> str:
76 | return 'render_' + template.hash
77 |
78 |
79 | def get_import_names(root: str, template_path: str):
80 | names = {os.path.join(root, template_path), os.path.basename(template_path)}
81 | pieces = template_path.split('/')
82 | for index in range(1, len(pieces)):
83 | names.add(os.path.sep.join(pieces[index:]))
84 | return list(names)
85 |
--------------------------------------------------------------------------------
/vibora/tests.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import asyncio
3 | from inspect import iscoroutinefunction
4 |
5 |
6 | def wrapper(f):
7 | def async_runner(*args, **kwargs):
8 | loop = asyncio.get_event_loop()
9 | loop.run_until_complete(f(*args, **kwargs))
10 | loop.run_until_complete(asyncio.sleep(0))
11 | loop.stop()
12 | loop.run_forever()
13 |
14 | return async_runner
15 |
16 |
17 | class TestSuite(unittest.TestCase):
18 |
19 | @classmethod
20 | def setUpClass(cls):
21 | for key, value in cls.__dict__.items():
22 | if key.startswith('test_') and iscoroutinefunction(value):
23 | setattr(cls, key, wrapper(value))
24 |
25 | @staticmethod
26 | async def _async_join(streaming):
27 | items = []
28 | async for chunk in streaming:
29 | items.append(chunk)
30 | try:
31 | if isinstance(items[0], bytes):
32 | return b''.join(items)
33 | except IndexError:
34 | return None
35 | return ''.join(items)
36 |
--------------------------------------------------------------------------------
/vibora/websockets/__init__.py:
--------------------------------------------------------------------------------
1 | from .obj import *
2 |
--------------------------------------------------------------------------------
/vibora/workers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vibora-io/vibora/4cda888f89aec6bfb2541ee53548ae1bf50fbf1b/vibora/workers/__init__.py
--------------------------------------------------------------------------------
/vibora/workers/handler.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import signal
3 | from socket import IPPROTO_TCP, TCP_NODELAY, SO_REUSEADDR, SOL_SOCKET, SO_REUSEPORT, socket
4 | from multiprocessing import Process
5 | from functools import partial
6 | from .reaper import Reaper
7 | from ..hooks import Events
8 | from ..utils import asynclib
9 |
10 |
11 | class RequestHandler(Process):
12 |
13 | def __init__(self, app, bind: str, port: int, sock=None):
14 | super().__init__()
15 | self.app = app
16 | self.bind = bind
17 | self.port = port
18 | self.daemon = True
19 | self.socket = sock
20 |
21 | def run(self):
22 |
23 | # Re-using address and ports. Kernel is our load balancer.
24 | if not self.socket:
25 | self.socket = socket()
26 | self.socket.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
27 | self.socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
28 | self.socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
29 | self.socket.bind((self.bind, self.port))
30 |
31 | # Creating a new event loop using a faster loop.
32 | loop = asynclib.new_event_loop()
33 | loop.app = self.app
34 | self.app.loop = loop
35 | self.app.components.add(loop)
36 | asyncio.set_event_loop(loop)
37 |
38 | # Starting the connection reaper.
39 | self.app.reaper = Reaper(app=self.app)
40 | self.app.reaper.start()
41 |
42 | # Registering routes, blueprints, handlers, callbacks, everything is delayed until now.
43 | self.app.initialize()
44 |
45 | # Calling before server start hooks (sync/async)
46 | loop.run_until_complete(self.app.call_hooks(Events.BEFORE_SERVER_START, components=self.app.components))
47 |
48 | # Creating the server.
49 | handler = partial(self.app.handler, app=self.app, loop=loop, worker=self)
50 | ss = loop.create_server(handler, sock=self.socket, reuse_port=True, backlog=1000)
51 |
52 | # Calling after server hooks (sync/async)
53 | loop.run_until_complete(ss)
54 | loop.run_until_complete(self.app.call_hooks(Events.AFTER_SERVER_START, components=self.app.components))
55 |
56 | async def stop_server(timeout=30):
57 |
58 | # Stop the reaper.
59 | self.app.reaper.has_to_work = False
60 |
61 | # Calling the before server stop hook.
62 | await self.app.call_hooks(Events.BEFORE_SERVER_STOP, components=self.app.components)
63 |
64 | # Ask all connections to finish as soon as possible.
65 | for connection in self.app.connections.copy():
66 | connection.stop()
67 |
68 | # Waiting all connections finish their tasks, after the given timeout
69 | # the connection will be closed abruptly.
70 | while timeout:
71 | all_closed = True
72 | for connection in self.app.connections:
73 | if not connection.is_closed():
74 | all_closed = False
75 | break
76 | if all_closed:
77 | break
78 | timeout -= 1
79 | await asyncio.sleep(1)
80 |
81 | loop.stop()
82 |
83 | def handle_kill_signal():
84 | self.socket.close() # Stop receiving new connections.
85 | loop.create_task(stop_server(10))
86 |
87 | try:
88 | loop.add_signal_handler(signal.SIGTERM, handle_kill_signal)
89 | loop.run_forever()
90 | except (SystemExit, KeyboardInterrupt):
91 | loop.stop()
92 |
--------------------------------------------------------------------------------
/vibora/workers/necromancer.py:
--------------------------------------------------------------------------------
1 | import threading
2 | import time
3 | from typing import Callable
4 |
5 |
6 | class Necromancer(threading.Thread):
7 |
8 | def __init__(self, app, spawn_function: Callable, interval: int=5):
9 | super().__init__()
10 | self.app = app
11 | self.spawn_function = spawn_function
12 | self.interval = interval
13 | self.must_work = True
14 |
15 | def run(self):
16 | while self.must_work:
17 | time.sleep(self.interval)
18 | workers_alive = []
19 | for worker in self.app.workers:
20 | if not worker.is_alive():
21 | worker = self.spawn_function()
22 | worker.start()
23 | workers_alive.append(worker)
24 | else:
25 | workers_alive.append(worker)
26 | self.app.workers = workers_alive
27 |
--------------------------------------------------------------------------------
/vibora/workers/reaper.py:
--------------------------------------------------------------------------------
1 | import time
2 | import os
3 | import signal
4 | from datetime import datetime, timezone
5 | from email.utils import formatdate
6 | from threading import Thread
7 | from ..responses import update_current_time
8 | from ..protocol import ConnectionStatus, update_current_time as update_time_protocol
9 |
10 |
11 | class Reaper(Thread):
12 | def __init__(self, app):
13 | super().__init__()
14 |
15 | self.app = app
16 |
17 | # Early bindings
18 | self.connections: set = self.app.connections
19 |
20 | # How long we allow a connection being idle.
21 | self.keep_alive_timeout: int = self.app.server_limits.keep_alive_timeout
22 |
23 | # In case the worker is stuck for some crazy reason (sync calls, expensive CPU ops) we gonna kill it.
24 | self.worker_timeout: int = self.app.server_limits.worker_timeout
25 |
26 | # Flag to stop this thread.
27 | self.has_to_work: bool = True
28 |
29 | @staticmethod
30 | async def kill_connections(connections: list):
31 | for connection in connections:
32 | connection.transport.clean_up()
33 |
34 | def check_if_worker_is_stuck(self):
35 | """
36 |
37 | :return:
38 | """
39 | current_time = time.time()
40 | for connection in self.app.connections.copy():
41 | conditions = (
42 | connection.get_status() == ConnectionStatus.PROCESSING_REQUEST,
43 | current_time - connection.get_last_task_time() >= self.worker_timeout
44 | )
45 | if all(conditions):
46 | # ###############
47 | # Seppuku #######
48 | # # # # # # # # #
49 | os.kill(os.getpid(), signal.SIGKILL)
50 |
51 | def kill_idle_connections(self):
52 | """
53 |
54 | :return:
55 | """
56 | now = time.time()
57 | for connection in self.connections.copy():
58 | if connection.get_status() == ConnectionStatus.PENDING and \
59 | (now - connection.get_last_task_time() > self.keep_alive_timeout):
60 | connection.stop()
61 |
62 | def run(self):
63 | """
64 |
65 | :return:
66 | """
67 | counter = 0
68 | while self.has_to_work:
69 | counter += 1
70 |
71 | # Removing the microseconds because this time is cached and it could trick the user into believing
72 | # that two requests were processed at exactly the same time because of the cached time.
73 | now = datetime.now(timezone.utc).replace(microsecond=0).astimezone()
74 | self.app.current_time = now.isoformat()
75 | update_current_time(formatdate(timeval=now.timestamp(), localtime=False, usegmt=True))
76 | update_time_protocol()
77 |
78 | if self.keep_alive_timeout > 0:
79 | if counter % self.keep_alive_timeout == 0:
80 | self.kill_idle_connections()
81 |
82 | if counter % self.worker_timeout == 0:
83 | self.check_if_worker_is_stuck()
84 |
85 | time.sleep(1)
86 |
--------------------------------------------------------------------------------