{"id":13705497,"url":"https://github.com/na-stewart/sanic-security","last_synced_at":"2025-12-12T00:37:43.482Z","repository":{"id":38302235,"uuid":"310976988","full_name":"na-stewart/sanic-security","owner":"na-stewart","description":"An async security library for the Sanic framework.","archived":false,"fork":false,"pushed_at":"2025-09-28T16:21:11.000Z","size":1569,"stargazers_count":98,"open_issues_count":0,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-14T13:07:19.348Z","etag":null,"topics":["aiohttp","async","auth","authentication","authorization","captcha","jwt","login","permissions","python","python3","register","roles","sanic","sanic-framework","security","session","token","two-factor","verification"],"latest_commit_sha":null,"homepage":"https://security.na-stewart.com","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/na-stewart.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-11-08T03:22:06.000Z","updated_at":"2025-10-06T19:24:33.000Z","dependencies_parsed_at":"2024-08-07T22:42:31.447Z","dependency_job_id":"c1bf75d5-b34b-4eaa-a2c1-e8a51a210c63","html_url":"https://github.com/na-stewart/sanic-security","commit_stats":null,"previous_names":["sunset-developer/sanic-security"],"tags_count":216,"template":false,"template_full_name":null,"purl":"pkg:github/na-stewart/sanic-security","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/na-stewart%2Fsanic-security","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/na-stewart%2Fsanic-security/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/na-stewart%2Fsanic-security/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/na-stewart%2Fsanic-security/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/na-stewart","download_url":"https://codeload.github.com/na-stewart/sanic-security/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/na-stewart%2Fsanic-security/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280325132,"owners_count":26311414,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-21T02:00:06.614Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aiohttp","async","auth","authentication","authorization","captcha","jwt","login","permissions","python","python3","register","roles","sanic","sanic-framework","security","session","token","two-factor","verification"],"created_at":"2024-08-02T22:00:42.533Z","updated_at":"2025-12-12T00:37:43.457Z","avatar_url":"https://github.com/na-stewart.png","language":"Python","funding_links":[],"categories":["Extensions"],"sub_categories":["Authentication"],"readme":"[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![Downloads](https://static.pepy.tech/badge/sanic-security)](https://pepy.tech/project/sanic-security)\n[![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/sanic-security.svg)](https://anaconda.org/conda-forge/sanic-security)\n\n\n\u003c!-- PROJECT LOGO --\u003e\n\u003cbr /\u003e\n\u003cp align=\"center\"\u003e\n  \u003ch3 align=\"center\"\u003eSanic Security\u003c/h3\u003e\n  \u003cp align=\"center\"\u003e\n   An async security library for the Sanic framework.\n  \u003c/p\u003e\n\u003c/p\u003e\n\n\n\u003c!-- TABLE OF CONTENTS --\u003e\n## Table of Contents\n\n* [About the Project](#about-the-project)\n* [Getting Started](#getting-started)\n  * [Installation](#installation)\n  * [Configuration](#configuration)\n* [Usage](#usage)\n    * [OAuth](#oauth)\n    * [Authentication](#authentication)\n    * [CAPTCHA](#captcha)\n    * [Two-step Verification](#two-step-verification)\n    * [Authorization](#authorization)\n    * [Testing](#testing)\n    * [Tortoise](#tortoise)\n* [Contributing](#contributing)\n* [License](#license)\n* [Versioning](#versioning)\n\n\u003c!-- ABOUT THE PROJECT --\u003e\n## About The Project\n\nSanic Security provides authentication, authorization, and verification for applications built with the \n[Sanic](https://github.com/sanic-org/sanic) web app framework.\n\n* OAuth2 integration\n* Login, registration, and authentication with refresh mechanisms\n* Role based authorization with wildcard permissions\n* Image \u0026 audio CAPTCHA\n* Two-step verification\n* Logging \u0026 auditing\n\nVisit [security.na-stewart.com](https://security.na-stewart.com) for documentation.\n\n\u003c!-- GETTING STARTED --\u003e\n## Getting Started\n\nIn order to get started, please install [PyPI](https://pypi.org/) (likely included with your Python build).\n\n### Installation\n\n* Install the Sanic Security package.\n```shell\npip3 install sanic-security\n````\n\n* Install the Sanic Security package with the [cryptography](https://github.com/pyca/cryptography) dependency included.\n\nIf you're planning on encoding or decoding JWTs using certain digital signature algorithms (like RSA or ECDSA which use \nthe public secret and private secret), you will need to install the `cryptography` library. This can be installed explicitly, or \nas an extra requirement.\n\n```shell\npip3 install sanic-security[crypto]\n````\n\n* Install the Sanic Security package with the [httpx-oauth](https://github.com/frankie567/httpx-oauth) dependency included.\n\nIf you're planning on utilizing OAuth, you will need to install the `httpx-oauth` library. This can be installed explicitly, or \nas an extra requirement.\n\n```shell\npip3 install sanic-security[oauth]\n````\n\n* Update Sanic Security if already installed.\n\n```shell\npip3 install sanic-security --upgrade\n```\n\n### Configuration\n\nSanic Security configuration is merely a `SimpleNamespace` that can be modified using dot-notation.\nFor example: \n\n```python\nfrom sanic_security.configuration import config as security_config\n\nsecurity_config.SECRET = \"This is a big secret. Shhhhh\"\nsecurity_config.CAPTCHA_FONT = \"resources/captcha-font.ttf\"\n```\n\nAny environment variables defined with the SANIC_SECURITY_ prefix will be applied to the config. For example, setting \nSANIC_SECURITY_SECRET will be loaded by the application automatically and fed into the SECRET config variable.\n\nYou can load environment variables with a different prefix via `security_config.load_environment_variables(\"NEW_PREFIX_\")` method.\n\n* Default configuration values:\n\n| Key                                   | Value                        | Description                                                                                                                       |\n|---------------------------------------|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|\n| **SECRET**                            | This is a big secret. Shhhhh | The secret used for generating and signing JWTs. This should be a string unique to your application. Keep it safe.                |\n| **PUBLIC_SECRET**                     | None                         | The secret used for verifying and decoding JWTs and can be publicly shared. This should be a string unique to your application.   |\n| **OAUTH_CLIENT**                      | None                         | The client ID provided by the OAuth provider, this is used to identify the application making the OAuth request.                  |\n| **OAUTH_SECRET**                      | None                         | The client secret provided by the OAuth provider, this is used in conjunction with the client ID to authenticate the application. |\n| **SESSION_SAMESITE**                  | Strict                       | The SameSite attribute of session cookies.                                                                                        |\n| **SESSION_SECURE**                    | True                         | The Secure attribute of session cookies.                                                                                          |\n| **SESSION_HTTPONLY**                  | True                         | The HttpOnly attribute of session cookies. HIGHLY recommended that you do not turn this off, unless you know what you are doing.  |\n| **SESSION_DOMAIN**                    | None                         | The Domain attribute of session cookies.                                                                                          |\n| **SESSION_ENCODING_ALGORITHM**        | HS256                        | The algorithm used to encode and decode session JWT's.                                                                            |\n| **SESSION_PREFIX**                    | tkn                          | Prefix attached to the beginning of session cookies.                                                                              |\n| **MAX_CHALLENGE_ATTEMPTS**            | 3                            | The maximum amount of session challenge attempts allowed.                                                                         |\n| **CAPTCHA_SESSION_EXPIRATION**        | 180                          | The amount of seconds till captcha session expiration on creation. Setting to 0 will disable expiration.                          |\n| **CAPTCHA_FONT**                      | captcha-font.ttf             | The file path to the font being used for captcha generation. Several fonts can be used by separating them via comma.              |\n| **CAPTCHA_VOICE**                     | captcha-voice/               | The directory of the voice library being used for audio captcha generation.                                                       |\n| **TWO_STEP_SESSION_EXPIRATION**       | 300                          | The amount of seconds till two-step session expiration on creation. Setting to 0 will disable expiration.                         |\n| **AUTHENTICATION_SESSION_EXPIRATION** | 86400                        | The amount of seconds till authentication session expiration on creation. Setting to 0 will disable expiration.                   |\n| **AUTHENTICATION_REFRESH_EXPIRATION** | 604800                       | The amount of seconds till authentication refresh expiration. Setting to 0 will disable refresh mechanism.                        |\n| **ALLOW_LOGIN_WITH_USERNAME**         | False                        | Allows login via username; unique constraint is disabled when set to false.                                                       |\n| **CLIENT_RECOGNITION**                | True                         | Client will be logged out if ip address is unrecognized.                                                                          |\n| **INITIAL_ADMIN_EMAIL**               | admin@example.com            | Email used when creating the initial admin account.                                                                               |\n| **INITIAL_ADMIN_PASSWORD**            | admin123                     | Password used when creating the initial admin account.                                                                            |\n\n## Usage\n\nSanic Security's authentication and verification functionality is session based. A new session will be created for the user after the user logs in or requests some form of verification (two-step, captcha). The session data is then encoded into a JWT and stored on a cookie on the user’s browser. The session cookie is then sent along with every subsequent request. The server can then compare the session stored on the cookie against the session information stored in the database to verify user’s identity and send a response with the corresponding state.\n\n* Initialize Sanic Security as follows:\n```python\ninitialize_security(app)\ninitialize_oauth(app)  # Remove if not utilizing OAuth\nif __name__ == \"__main__\":\n    app.run(host=\"127.0.0.1\", port=8000, workers=1, debug=True)\n```\n\nThe tables in the below examples represent example [request form-data](https://sanicframework.org/en/guide/basics/request.html#form).\n\n## OAuth\n\nProvides users with a familiar experience by having them register/login using their existing credentials from other trusted services (such as Google, Discord, etc.).\n\nThis feature is designed to complement existing protocols by linking Sanic Security with the user's OAuth credentials. As a result, developers can leverage robust session handling and account management.\n\n* Define OAuth clients\n\nYou can [utilize various OAuth clients](https://frankie567.github.io/httpx-oauth/reference/httpx_oauth.clients/) based on your needs or [customize one](https://frankie567.github.io/httpx-oauth/usage/).\nID and secret should be stored and referenced via configuration.\n\n```python\ndiscord_oauth = DiscordOAuth2(\n    \"1325594509043830895\",\n    \"WNMYbkDJjGlC0ej60qM-50tC9mMy0EXa\",\n)\ngoogle_oauth = GoogleOAuth2(\n    \"480512993828-e2e9tqtl2b8or62hc4l7hpoh478s3ni1.apps.googleusercontent.com\",\n    \"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\",\n)\n```\n\n* Redirect to authorization URL\n\n```python\n@app.route(\"api/security/oauth\", methods=[\"GET\", \"POST\"])\nasync def on_oauth_request(request):\n    return redirect(\n        await google_oauth.get_authorization_url(\n            \"http://localhost:8000/api/security/oauth/callback\",\n            scope=google_oauth.base_scopes,\n        )\n    )\n```\n\n* Handle OAuth callback\n\n```python\n@app.get(\"api/security/oauth/callback\")\nasync def on_oauth_callback(request):\n    token_info, authentication_session = await oauth_callback(\n        request, google_oauth, \"http://localhost:8000/api/security/oauth/callback\"\n    )\n    response = json(\n        \"Authorization successful.\",\n        {\"token_info\": token_info, \"auth_session\": authentication_session.json},\n    )\n    oauth_encode(response, token_info)\n    authentication_session.encode(response)\n    return response\n```\n\n* Get access token \n\n```python\n@app.get(\"api/security/oauth/token\")\nasync def on_oauth_token(request):\n    token_info = await decode_oauth(request, google_oauth)\n    return json(\n        \"Access token retrieved.\",\n        token_info,\n    )\n```\n\n* Requires access token (This method is not called directly and instead used as a decorator)\n\n```python\n@app.get(\"api/security/oauth/token\")\n@requires_oauth(google_oauth)\nasync def on_oauth_token(request):\n    return json(\n        \"Access token retrieved.\",\n        request.ctx.oauth,\n    )\n```\n\n## Authentication\n  \n* Registration (with two-step email verification)\n\nPhone can be null or empty.\n\n| Key          | Value               |\n|--------------|---------------------|\n| **username** | example             |\n| **email**    | example@example.com |\n| **phone**    | 19811354186         |\n| **password** | examplepass         |\n\n```python\n@app.post(\"api/security/register\")\nasync def on_register(request):\n    account = await register(request)\n    two_step_session = await request_two_step_verification(request, account)\n    await email_code(\n        account.email, two_step_session.code  # Code = 24KF19\n    )  # Custom method for emailing verification code.\n    response = json(\n        \"Registration successful! Email verification required.\", account.json\n    )\n    two_step_session.encode(response)\n    return response\n```\n\n* Verify Account\n\nVerifies the client's account via two-step session code.\n\n| Key      | Value  |\n|----------|--------|\n| **code** | 24KF19 |\n\n```python\n@app.put(\"api/security/verify\")\nasync def on_verify(request):\n    two_step_session = await verify_account(request)\n    return json(\"You have verified your account and may login!\", two_step_session.json)\n```\n\n* Login (with two-step email verification)\n\nCredentials are retrieved via header are constructed by first combining the username and the password with a colon \n(aladdin:opensesame), and then by encoding the resulting string in base64 (YWxhZGRpbjpvcGVuc2VzYW1l). \nHere is an example authorization header: `Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l`. \n\nIf this isn't desired, you can pass credentials into the login method instead.\n\nYou can use a username as well as an email for login if `ALLOW_LOGIN_WITH_USERNAME` is true in the config.\n\n```python\n@app.post(\"api/security/login\")\nasync def on_login(request):\n    authentication_session = await login(request, require_second_factor=True)\n    two_step_session = await request_two_step_verification(\n        request, authentication_session.bearer, \"2fa\"\n    )\n    await email_code(\n        authentication_session.bearer.email, two_step_session.code  # Code = XGED2U\n    )  # Custom method for emailing verification code.\n    response = json(\n        \"Login successful! Two-factor authentication required.\",\n        authentication_session.json,\n    )\n    authentication_session.encode(response)\n    two_step_session.encode(response)\n    return response\n```\n\n* Fulfill Second Factor\n\nFulfills client authentication session's second factor requirement via two-step session code.\n\n| Key      | Value  |\n|----------|--------|\n| **code** | XGED2U |\n\n```python\n@app.put(\"api/security/fulfill-2fa\")\nasync def on_two_factor_authentication(request):\n    authentication_session = await fulfill_second_factor(request)\n    response = json(\n        \"Authentication session second-factor fulfilled! You are now authenticated.\",\n        authentication_session.json,\n    )\n    return response\n```\n\n* Anonymous Login\n\nSimply create a new session and encode it.\n\n```python\n@app.post(\"api/security/login/anon\")\nasync def on_anonymous_login(request):\n    authentication_session = await AuthenticationSession.new(request)\n    response = json(\n        \"Anonymous client now associated with session!\", authentication_session.json\n    )\n    authentication_session.encode(response)\n    return response\n```\n\n* Logout\n\n```python\n@app.post(\"api/security/logout\")\nasync def on_logout(request):\n    authentication_session = await logout(request)\n    token_info = await oauth_revoke(\n        request, google_oauth\n    )  # Remove if not utilizing OAuth\n    response = json(\n        \"Logout successful!\",\n        {\"token_info\": token_info, \"auth_session\": authentication_session.json},\n    )\n    return response\n```\n\n* Authenticate\n\n```python\n@app.post(\"api/security/auth\")\nasync def on_authenticate(request):\n    authentication_session = await authenticate(request)\n    response = json(\n        \"You have been authenticated.\",\n        authentication_session.json,\n    )\n    return response\n```\n\n* Requires Authentication (this method is not called directly and instead used as a decorator)\n\n```python\n@app.post(\"api/security/auth\")\n@requires_authentication\nasync def on_authenticate(request):\n    response = json(\"You have been authenticated.\", request.ctx.session.json)\n    return response\n```\n\n## CAPTCHA\n\nProtects against spam and malicious activities by ensuring that only real humans can complete certain actions like \nsubmitting a form or creating an account. A font and voice library for CAPTCHA challenges is included in the repository, \nor you can download/create your own and specify its path in the configuration.\n\n* Request CAPTCHA\n\n```python\n@app.get(\"api/security/captcha\")\nasync def on_captcha_img_request(request):\n    captcha_session = await CaptchaSession.new(request)\n    response = raw(\n        captcha_session.get_image(), content_type=\"image/jpeg\"\n    )  # Captcha: LJ0F3U\n    captcha_session.encode(response)\n    return response\n```\n\n* Request CAPTCHA Audio\n\n```python\n@app.get(\"api/security/captcha/audio\")\nasync def on_captcha_audio_request(request):\n    captcha_session = await CaptchaSession.decode(request)\n    return raw(captcha_session.get_audio(), content_type=\"audio/mpeg\")\n```\n\n* Attempt CAPTCHA\n\n| Key         | Value  |\n|-------------|--------|\n| **captcha** | LJ0F3U |\n\n```python\n@app.post(\"api/security/captcha\")\nasync def on_captcha(request):\n    captcha_session = await captcha(request)\n    return json(\"Captcha attempt successful!\", captcha_session.json)\n```\n\n* Requires CAPTCHA (this method is not called directly and instead used as a decorator)\n\n| Key         | Value  |\n|-------------|--------|\n| **captcha** | LJ0F3U |\n\n```python\n@app.post(\"api/security/captcha\")\n@requires_captcha\nasync def on_captcha(request):\n    return json(\"Captcha attempt successful!\", request.ctx.session.json)\n```\n\n## Two-step Verification\n\nTwo-step verification should be integrated with other custom functionalities, such as forgot password recovery.\n\n* Request Two-step Verification\n\n| Key         | Value               |\n|-------------|---------------------|\n| **email**   | example@example.com |\n\n```python\n@app.post(\"api/security/two-step/request\")\nasync def on_two_step_request(request):\n    two_step_session = await request_two_step_verification(\n        request, tag=\"forgot-pass\"\n    )  # Code = T2I58I\n    await email_code(\n        two_step_session.bearer.email, two_step_session.code\n    )  # Custom method for emailing verification code.\n    response = json(\"Verification request successful!\", two_step_session.json)\n    two_step_session.encode(response)\n    return response\n``` \n\n* Resend Two-step Verification Code\n\n```python\n@app.post(\"api/security/two-step/resend\")\nasync def on_two_step_resend(request):\n    two_step_session = await TwoStepSession.decode(request)  # Code = T2I58I\n    await email_code(\n        two_step_session.bearer.email, two_step_session.code\n    )  # Custom method for emailing verification code.\n    return json(\"Verification code resend successful!\", two_step_session.json)\n```\n\n* Attempt Two-step Verification\n\n| Key      | Value  |\n|----------|--------|\n| **code** | T2I58I |\n\n```python\n@app.post(\"api/security/two-step\")\nasync def on_two_step_verification(request):\n    two_step_session = await two_step_verification(request, \"forgot-pass\")\n    response = json(\"Two-step verification attempt successful!\", two_step_session.json)\n    return response\n```\n\n* Requires Two-step Verification (this method is not called directly and instead used as a decorator)\n\n| Key      | Value  |\n|----------|--------|\n| **code** | T2I58I |\n\n```python\n@app.post(\"api/security/two-step\")\n@requires_two_step_verification(\"forgot-pass\")\nasync def on_two_step_verification(request):\n    response = json(\n        \"Two-step verification attempt successful!\", request.ctx.session.json\n    )\n    return response\n```\n\n## Authorization\n\nSanic Security uses role based authorization with wildcard permissions.\n\nRoles are created for various job functions. The permissions to perform certain operations are assigned to specific roles. \nUsers are assigned particular roles, and through those role assignments acquire the permissions needed to perform \nparticular system functions. Since users are not assigned permissions directly, but only acquire them through their \nrole (or roles), management of individual user rights becomes a matter of simply assigning appropriate roles to the \nuser's account; this simplifies common operations, such as adding a user, or changing a user's department. \n\nWildcard permissions support the concept of multiple levels or parts. For example, you could grant a user the permission\n`printer:query`, `printer:query,delete`, or `printer:*`.\n\n* Assign Role\n\n```python\nawait assign_role(\n    \"Chat Room Moderator\",\n    account,\n    \"Can read and delete messages in all chat rooms, suspend and mute accounts, and control voice chat.\",\n    \"channels:view,delete\",\n    \"voice:*\",\n    \"account:suspend,mute\",\n)\n```\n\n* Check Permissions\n\n```python\n@app.post(\"api/security/perms\")\nasync def on_check_perms(request):\n    authentication_session = await check_permissions(\n        request, \"channels:view\", \"voice:*\"\n    )\n    return json(\"Account is authorized.\", authentication_session.json)\n```\n\n* Requires Permission (this method is not called directly and instead used as a decorator)\n\n```python\n@app.post(\"api/security/perms\")\n@requires_permission(\"channels:view\", \"voice:*\")\nasync def on_check_perms(request):\n    return json(\"Account is authorized.\", request.ctx.session.json)\n```\n\n* Check Roles\n\n```python\n@app.post(\"api/security/roles\")\nasync def on_check_roles(request):\n    authentication_session = await check_roles(request, \"Chat Room Moderator\")\n    return json(\"Account is authorized.\", authentication_session.json)\n```\n\n* Requires Role (This method is not called directly and instead used as a decorator)\n\n```python\n@app.post(\"api/security/roles\")\n@requires_role(\"Chat Room Moderator\")\nasync def on_check_roles(request):\n    return json(\"Account is authorized.\", request.ctx.session.json)\n```\n\n## Testing\n\n* Set the `TEST_DATABASE_URL` configuration value.\n\n* Make sure the test Sanic instance (`test/server.py`) is running on your machine.\n\n* Run the test client (`test/tests.py`) for results.\n\n## Tortoise\n\nSanic Security uses [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/index.html) for database operations.\n\nTortoise ORM is an easy-to-use asyncio ORM (Object Relational Mapper).\n\n* Initialise your models and database like so: \n\n```python\nasync def init():\n    await Tortoise.init(\n        db_url=\"sqlite://db.sqlite3\",\n        modules={\"models\": [\"sanic_security.models\", \"app.models\"]},\n    )\n    await Tortoise.generate_schemas()\n```\n\nor\n\n```python\nregister_tortoise(\n    app,\n    db_url=\"sqlite://db.sqlite3\",\n    modules={\"models\": [\"sanic_security.models\", \"app.models\"]},\n    generate_schemas=True,\n)\n```\n\n* Define your models like so:\n\n```python\nfrom tortoise.models import Model\nfrom tortoise import fields\n\n\nclass Tournament(Model):\n    id = fields.IntField(pk=True)\n    name = fields.TextField()\n```\n\n* Use it like so:\n\n```python\n# Create instance by save\ntournament = Tournament(name=\"New Tournament\")\nawait tournament.save()\n\n# Or by .create()\nawait Tournament.create(name=\"Another Tournament\")\n\n# Now search for a record\ntour = await Tournament.filter(name__contains=\"Another\").first()\nprint(tour.name)\n```\n\n\u003c!-- CONTRIBUTING --\u003e\n## Contributing\n\nContributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.\n\n1. Fork the Project\n2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the Branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n\n\u003c!-- LICENSE --\u003e\n## License\n\nDistributed under the MIT License. See `LICENSE` for more information.\n\n\u003c!-- Versioning --\u003e\n## Versioning\n\n**x.x.x**\n\n* MAJOR version when you make incompatible API changes.\n\n* MINOR version when you add functionality in a backwards compatible manner.\n\n* PATCH version when you make backwards compatible bug fixes.\n\n[https://semver.org/](https://semver.org/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fna-stewart%2Fsanic-security","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fna-stewart%2Fsanic-security","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fna-stewart%2Fsanic-security/lists"}