├── .env-example
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── pythonapp.yml
│ └── pythonpublish.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── codecov.yml
├── craft
├── makefile
├── masonite.sqlite3
├── pyproject.toml
├── pytest.ini
├── requirements.txt
├── setup.cfg
├── setup.py
├── src
├── __init__.py
└── multitenancy
│ ├── __init__.py
│ ├── commands
│ ├── TenancyCreate.py
│ ├── TenancyDelete.py
│ ├── TenancyList.py
│ ├── TenancyMigrate.py
│ ├── TenancyMigrateRefresh.py
│ ├── TenancyMigrateReset.py
│ ├── TenancyMigrateRollback.py
│ ├── TenancyMigrateStatus.py
│ └── TenancySeed.py
│ ├── config
│ └── multitenancy.py
│ ├── contexts
│ ├── TenantContext.py
│ └── __init__.py
│ ├── facades
│ ├── Tenancy.py
│ ├── Tenancy.pyi
│ └── __init__.py
│ ├── middlewares
│ └── tenant_finder_middleware.py
│ ├── migrations
│ └── 2022_07_07_230413_create_tenants_table.py
│ ├── models
│ └── Tenant.py
│ ├── multitenancy.py
│ └── providers
│ ├── __init__.py
│ └── multitenancy_provider.py
├── tenant1.sqlite3
├── tenant2.sqlite3
├── tests
├── __init__.py
├── integrations
│ ├── Kernel.py
│ ├── app
│ │ ├── __init__.py
│ │ ├── commands
│ │ │ └── __init__.py
│ │ ├── controllers
│ │ │ ├── WelcomeController.py
│ │ │ └── __init__.py
│ │ ├── middlewares
│ │ │ ├── AuthenticationMiddleware.py
│ │ │ ├── VerifyCsrfToken.py
│ │ │ └── __init__.py
│ │ └── models
│ │ │ └── User.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── application.py
│ │ ├── auth.py
│ │ ├── broadcast.py
│ │ ├── cache.py
│ │ ├── database.py
│ │ ├── exceptions.py
│ │ ├── filesystem.py
│ │ ├── mail.py
│ │ ├── multitenancy.py
│ │ ├── notification.py
│ │ ├── providers.py
│ │ ├── queue.py
│ │ └── session.py
│ ├── databases
│ │ ├── migrations
│ │ │ ├── 2021_01_09_033202_create_password_reset_table.py
│ │ │ ├── 2021_01_09_043202_create_users_table.py
│ │ │ └── 2022_07_07_230413_create_tenants_table.py
│ │ └── seeds
│ │ │ ├── __init__.py
│ │ │ ├── database_seeder.py
│ │ │ └── user_table_seeder.py
│ ├── resources
│ │ ├── css
│ │ │ └── app.css
│ │ └── js
│ │ │ ├── app.js
│ │ │ └── bootstrap.js
│ ├── routes
│ │ └── web.py
│ ├── storage
│ │ ├── .gitignore
│ │ └── public
│ │ │ ├── favicon.ico
│ │ │ ├── logo.png
│ │ │ └── robots.txt
│ └── templates
│ │ ├── __init__.py
│ │ ├── base.html
│ │ ├── errors
│ │ ├── 403.html
│ │ ├── 404.html
│ │ └── 500.html
│ │ ├── maintenance.html
│ │ └── welcome.html
└── unit
│ ├── __init__.py
│ └── test_package.py
└── wsgi.py
/.env-example:
--------------------------------------------------------------------------------
1 | APP_DEBUG=True
2 | APP_ENV=development
3 | APP_KEY=plyUWY8iZnEH9_8WrVjl-LS3B8aRtHK9UAB35fGAq0M=
4 | DB_CONFIG_PATH=tests/integrations/config/database
5 | DB_CONNECTION=sqlite
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for GitHub Actions
4 | - package-ecosystem: "github-actions"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 |
9 | # Maintain dependencies for cookiecutter repo
10 | - package-ecosystem: "pip"
11 | directory: "/"
12 | schedule:
13 | interval: "weekly"
14 | # Allow up to 10 open pull requests for pip dependencies
15 | open-pull-requests-limit: 10
16 |
--------------------------------------------------------------------------------
/.github/workflows/pythonapp.yml:
--------------------------------------------------------------------------------
1 | name: Test Application
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.7", "3.8", "3.9", "3.10"]
11 | name: Python ${{ matrix.python-version }}
12 | steps:
13 | - uses: actions/checkout@v4
14 | - name: Set up Python ${{ matrix.python-version }}
15 | uses: actions/setup-python@v4
16 | with:
17 | python-version: ${{ matrix.python-version }}
18 | - name: Install dependencies
19 | run: |
20 | make init
21 | - name: Test with pytest and Build coverage
22 | run: |
23 | make coverage
24 | - name: Upload coverage
25 | uses: codecov/codecov-action@v3
26 | with:
27 | token: ${{ secrets.CODECOV_TOKEN }}
28 | fail_ci_if_error: false
29 |
30 | lint:
31 | runs-on: ubuntu-latest
32 | name: Lint
33 | steps:
34 | - uses: actions/checkout@v4
35 | - name: Set up Python 3.8
36 | uses: actions/setup-python@v4
37 | with:
38 | python-version: 3.8
39 | - name: Intall Flake8
40 | run: |
41 | pip install flake8
42 | - name: Lint
43 | run: make lint
44 |
--------------------------------------------------------------------------------
/.github/workflows/pythonpublish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Python
13 | uses: actions/setup-python@v4
14 | with:
15 | python-version: "3.x"
16 | - name: Install dependencies
17 | run: |
18 | make init
19 | - name: Publish only packages passing test
20 | run: |
21 | make test
22 | - name: Build and publish
23 | env:
24 | TWINE_USERNAME: __token__
25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
26 | run: |
27 | make publish
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv
2 | venv2
3 | .vscode
4 | .idea/
5 | build/
6 | .pypirc
7 | .coverage
8 | coverage.xml
9 | .pytest_*
10 | **/*__pycache__*
11 | **/*.DS_Store*
12 | **.pyc
13 | dist
14 | .env
15 | *.db
16 | src/masonite_multitenancy.egg-info
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guide
2 |
3 | ## Introduction
4 |
5 | When contributing to this repository, **please first discuss the change you wish to make via issue, email, or any other method with the owners or contributors of this repository** before making a change 😃 . Thank you !
6 |
7 | ## Getting Started
8 |
9 | ### Get the code
10 |
11 | First you should configure your local environment to be able to make changes in this package.
12 |
13 | 1. Fork the `https://github.com/yubarajshrestha/masonite-multitenancy` repo.
14 | 2. Clone that repo into your computer: `git clone http://github.com/your-username/multitenancy.git`.
15 | 3. Checkout the current release branch \(example: `master`\).
16 | 4. Run `git pull origin master` to get the current release version.
17 |
18 | ### Install the environment
19 |
20 | 1. You should create a Python virtual environment with `Python >= 3.6`.
21 | 2. Then install the dependencies and setup the project, in root directory with:
22 |
23 | ```
24 | make init
25 | ```
26 |
27 | **Note:**
28 |
29 | - The package will be locally installed in your venv (with `pip install .`). Meaning that you will be
30 | able to import it from the project contained in the package as if you installed it from PyPi.
31 | - When making changes to your packages you will need to uninstall the package and reinstall it with
32 | `pip uninstall multitenancy && pip install .`
33 |
34 | ### Contribute
35 |
36 | - From there simply create:
37 | - a feature branch `feat/my-new-feature`
38 | - a fix branch `fix/my-new-fix`
39 | - Push to your origin repository:
40 | - `git push origin feat/my-new-feature`
41 | - Open a pull request (PR) and follow the PR process below
42 |
43 | 1. You should open an issue before making any pull requests. Not all features will be added to the package and some may be better off as another third party package. It wouldn't be good if you worked on a feature for several days and the pull request gets rejected for reasons that could have been discussed in an issue for several minutes.
44 | 2. Ensure any changes are well commented and any configuration files that are added have a flagpole comment on the variables it's setting.
45 | 3. Update the README.md if installation/configuration or usage has changed.
46 | 4. It's better to add unit tests for the changes you made.
47 | 5. The PR must pass Github CI checks. The PR can be merged in once you have a successful review from a maintainer.
48 | 6. The version will be bumped by the maintainer when merging, so don't edit package version in the PR.
49 |
50 | ### Testing
51 |
52 | - To add unit tests add tests under `tests/` directory, please read about [Masonite
53 | testing](https://docs.masoniteproject.com/useful-features/testing) in the official
54 | documentation
55 |
56 | - To test your package locally in a project, a default Masonite project is available
57 | at root. Just run `python craft serve` and navigate to `localhost:8000/` and
58 | you will see `Hello Package World` in your browser.
59 |
60 | ## Dev Guidelines
61 |
62 | ### Package development
63 |
64 | You should read guidelines on package creation in the [Official Documentation](https://docs.masoniteproject.com/advanced/creating-packages)
65 |
66 | ### Comments
67 |
68 | Comments are a vital part of any repository and should be used where needed. It is important not to overcomment something. If you find you need to constantly add comments, you're code may be too complex. Code should be self documenting \(with clearly defined variable and method names\)
69 |
70 | #### Types of comments to use
71 |
72 | There are 3 main type of comments you should use when developing for Masonite:
73 |
74 | **Module Docstrings**
75 |
76 | All modules should have a docstring at the top of every module file and should look something like:
77 |
78 | ```python
79 | """ This is a module to add support for Billing users """
80 | from masonite.request import Request
81 | ...
82 | ```
83 |
84 | **Method and Function Docstrings**
85 |
86 | All methods and functions should also contain a docstring with a brief description of what the module does
87 |
88 | For example:
89 |
90 | ```python
91 | def some_function(self):
92 | """
93 | This is a function that does x action.
94 | Then give an exmaple of when to use it
95 | """
96 | ... code ...
97 | ```
98 |
99 | **Code Comments**
100 |
101 | If you're code MUST be complex enough that future developers will not understand it, add a `#` comment above it
102 |
103 | For normal code this will look something like:
104 |
105 | ```python
106 | # This code performs a complex task that may not be understood later on
107 | # You can add a second line like this
108 | complex_code = 'value'
109 |
110 | perform_some_complex_task()
111 | ```
112 |
113 | **Flagpole Comments**
114 |
115 | Flag pole comments are a fantastic way to give developers an inside to what is really happening and for now should only be reserved for configuration files. A flag pole comment gets its name from how the comment looks
116 |
117 | ```text
118 | """
119 | |--------------------------------------------------------------------------
120 | | A Heading of The Setting Being Set
121 | |--------------------------------------------------------------------------
122 | |
123 | | A quick description
124 | |
125 | """
126 |
127 | SETTING = "some value"
128 | ```
129 |
130 | It's important to note that there should have exactly 75 `-` above and below the header and have a trailing `|` at the bottom of the comment.
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022, Yubaraj Shrestha
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 NONINFRINGEMENT. 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.
22 |
23 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/MANIFEST.in
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | # Masonite Multitenancy (WIP)
18 |
19 | Multitenancy package for Masonite!
20 |
21 | Multitenancy is a feature that allows you to have multiple tenants in your application. This is useful for things like a company having multiple websites, or a company having multiple apps.
22 |
23 | ### Features
24 |
25 | - [x] Create a new tenant (with domain)
26 | - [x] Tenant specific configurations
27 | - [x] Tenant specific migrations and seeders
28 | - [x] Tenant middleware to specify tenant in request on the fly
29 |
30 | ### Installation
31 |
32 | ```bash
33 | pip install masonite-multitenancy
34 | ```
35 |
36 | ### Configuration
37 |
38 | Add _`MultitenancyProvider`_ to your project in `config/providers.py`:
39 |
40 | ```python
41 | # config/providers.py
42 | # ...
43 | from multitenancy import MultitenancyProvider
44 |
45 | # ...
46 | PROVIDERS = [
47 | # ...
48 | # Third Party Providers
49 | MultitenancyProvider,
50 | # ...
51 | ]
52 | ```
53 |
54 | Then you can publish the package resources (if needed) by doing:
55 |
56 | ```bash
57 | python craft package:publish multitenancy
58 | ```
59 |
60 | ### Command Usage
61 |
62 | You'll get bunch of commands to manage tenants.
63 |
64 | **Create a new tenant**
65 |
66 | This will prompt few questions just provider answers and that's it.
67 |
68 | ```bash
69 | python craft tenancy:create
70 | ```
71 |
72 | This will also automatically generate new database connection based on your `default` database connection from `config/database.py`.
73 |
74 | **List all tenants**
75 |
76 | ```bash
77 | python craft tenancy:list
78 | ```
79 |
80 | **Delete a tenant**
81 |
82 | ```bash
83 | # delete a tenant by database name
84 | python craft tenancy:delete --tenants=tenant1
85 | # or
86 | python craft tenancy:delete --tenants=tenant1,tenant2
87 | ```
88 |
89 | **Delete all tenants**
90 |
91 | ```bash
92 | python craft tenancy:delete
93 | ```
94 |
95 | **Migrate a tenant**
96 |
97 | ```bash
98 | python craft tenancy:migrate --tenants=tenant1
99 | # or
100 | python craft tenancy:migrate --tenants=tenant1,tenant2
101 | ```
102 |
103 | **Migrate all tenants**
104 |
105 | ```bash
106 | python craft tenancy:migrate
107 | ```
108 |
109 | Similary you can use `tenancy:migrate:refresh`, `tenancy:migrate:reset`, `tenancy:migrate:status` and `tenancy:migrate:rollback` commands.
110 | All commands will take `--tenants` option to specify tenants if you ever need.
111 |
112 | **Seed a tenant**
113 |
114 | ```bash
115 | python craft tenancy:seed --tenants=tenant1
116 | # or
117 | python craft tenancy:seed --tenants=tenant1,tenant2
118 | ```
119 |
120 | **Seed all tenants**
121 |
122 | ```bash
123 | python craft tenancy:seed
124 | ```
125 |
126 | ### Using Tenancy Facade
127 |
128 | **Create a new tenant**
129 |
130 | ```python
131 | from multitenancy.facades import Tenancy
132 |
133 | # creates a new tenant and returns instance of new Tenant
134 | Tenancy.create(
135 | name='tenant1',
136 | domain='tenant1.example.com',
137 | database='tenant1',
138 | )
139 | ```
140 |
141 | **Get tenant**
142 |
143 | ```python
144 | from multitenancy.facades import Tenancy
145 |
146 | # by id
147 | Tenancy.get_tenant_by_id(1)
148 |
149 | # by domain
150 | Tenancy.get_tenant_by_domain('tenant1.example.com')
151 |
152 | # by database name
153 | Tenancy.get_tenant_by_database('tenant1')
154 | ```
155 |
156 | **Delete tenant**
157 |
158 | ```python
159 | from multitenancy.facades import Tenancy
160 |
161 |
162 | tenant = Tenant.find(1)
163 | Tenancy.delete(tenant)
164 | ```
165 |
166 | **Connections**
167 |
168 | ```python
169 | from multitenancy.facades import Tenancy
170 |
171 | # setting tenant specific connection
172 | tenant = Tenant.find(1)
173 | Tenancy.set_connection(tenant)
174 |
175 | # resetting to default connection
176 | Tenancy.reset_connection()
177 | ```
178 |
179 | Event though above approach can be used to set tenant specific connection, and do tenant related tasks, it's recommended to use `TenantContext` instead.
180 |
181 | ### Using Tenant Context
182 |
183 | You might sometime need to get data from different tenant in your application or you might have to do some logic based on tenant. In this case you can use `TenantContext` class to get tenant data.
184 |
185 | ```python
186 | from multitenancy.contexts import TenantContext
187 | from multitenancy.models.Tenant import Tenant
188 |
189 | tenant = Tenant.where('name', '=', 'tenant1').first()
190 |
191 | with TenantContext(tenant=tenant):
192 | # do something with tenant1 data
193 | # ...
194 | ```
195 |
196 | You can also do all other tenant specific tasks like: `migrations`, `seeds`.
197 |
198 | ```python
199 | from multitenancy.contexts import TenantContext
200 | from multitenancy.models.Tenant import Tenant
201 |
202 | tenant = Tenant.where('name', '=', 'tenant1').first()
203 |
204 | with TenantContext(tenant=tenant) as ctx:
205 | # migrate the database
206 | ctx.migrate()
207 | ctx.migrate_refresh()
208 | ctx.migrate_rollback()
209 | ctx.migrate_reset()
210 | ctx.migrate_status()
211 |
212 | # seed the database
213 | ctx.seed()
214 | ```
215 |
216 | ### Final Step
217 |
218 | Now the multitenancy is almost ready to use. The final step is to make use of tenancy middleware. This middleware will be used to specify tenant in request on the fly. So, basically you have to attach this middleware to all the routes that are tenant aware.
219 |
220 | ```python
221 | # config/routes.py
222 | # ...
223 |
224 | Route.get("/", "WelcomeController@show")
225 | Route.get("/tenant-aware-routes", "WelcomeController@show").middleware("multitenancy")
226 | ```
227 |
228 | In above example, `/tenant-aware-routes` will be tenant aware. It means that if you have tenant setup and you are trying to access `/tenant-aware-routes` then you will get tenant specific items from the database.
229 |
230 | ### TODO
231 |
232 | - [x] Different database server for each tenant
233 |
234 | ### Contributing
235 |
236 | Please read the [Contributing Documentation](CONTRIBUTING.md) here.
237 |
238 | ### Maintainers
239 |
240 | - [x] [Yubaraj Shrestha](https://www.github.com/yubarajshrestha)
241 |
242 | ### License
243 |
244 | multitenancy is open-sourced software licensed under the [MIT license](LICENSE).
245 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: no
3 |
4 | github_checks: false
5 |
6 | coverage:
7 | precision: 2
8 | round: down
9 | range: "70...100"
10 |
11 | parsers:
12 | gcov:
13 | branch_detection:
14 | conditional: yes
15 | loop: yes
16 | method: no
17 | macro: no
18 |
19 | comment:
20 | layout: "footer"
21 | behavior: default
22 | require_changes: no
23 |
--------------------------------------------------------------------------------
/craft:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Craft Command.
3 | This module is really used for backup only if the masonite CLI cannot import this for you.
4 | This can be used by running "python craft". This module is not ran when the CLI can
5 | successfully import commands for you.
6 | """
7 |
8 | from wsgi import application
9 |
10 | if __name__ == '__main__':
11 | application.make('commands').run()
12 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | .PHONY: help
2 | help: ## Show this help
3 | @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
4 |
5 | init: ## Install package dependencies
6 | cp .env-example .env
7 | pip install --upgrade pip
8 | # install test project and package dependencies
9 | pip install -r requirements.txt
10 | # install package and dev dependencies (see setup.py)
11 | pip install '.[dev]'
12 | test: ## Run package tests
13 | python -m pytest tests
14 | ci: ## [CI] Run package tests and lint
15 | make test
16 | make lint
17 | lint: ## Run code linting
18 | python -m flake8 .
19 | format: ## Format code with Black
20 | black src
21 | coverage: ## Run package tests and upload coverage reports
22 | python -m pytest --cov-report term --cov-report xml --cov=src/masonite/multitenancy tests
23 | publish: ## Publish package to pypi
24 | python setup.py sdist bdist_wheel
25 | twine upload dist/*
26 | rm -fr build dist .egg src/masonite_multitenancy.egg-info
27 | pypirc: ## Copy the template .pypirc in the repo to your home directory
28 | cp .pypirc ~/.pypirc
--------------------------------------------------------------------------------
/masonite.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/masonite.sqlite3
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 99
3 | target-version = ['py38']
4 | include = '\.pyi?$'
5 | exclude = '''
6 | /(
7 | \.git
8 | \.github
9 | \.vscode
10 | | \.venv
11 | | docs
12 | | node_modules
13 | | tests/integrations/templates
14 | )/
15 | '''
16 |
17 | [tool.isort]
18 | profile = "black"
19 | multi_line_output = 3
20 | include_trailing_comma = true
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | filterwarnings =
3 | ignore::DeprecationWarning
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | masonite>=4,<5
2 | masonite-orm>=2,<3
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude =
3 | .git,
4 | .github,
5 | .vscode,
6 | __pycache__,
7 | templates,
8 | node_modules,
9 | venv
10 | max-complexity = 10
11 | max-line-length = 99
12 |
13 | omit =
14 | */config/*
15 | setup.py
16 | */stubs/*
17 | wsgi.py
18 | tests/
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setup(
7 | name="masonite-multitenancy",
8 | # Versions should comply with PEP440. For a discussion on single-sourcing
9 | # the version across setup.py and the project code, see
10 | # https://packaging.python.org/en/latest/single_source_version.html
11 | version="0.0.5",
12 | packages=[
13 | "multitenancy",
14 | "multitenancy.commands",
15 | "multitenancy.config",
16 | "multitenancy.middlewares",
17 | "multitenancy.migrations",
18 | "multitenancy.models",
19 | "multitenancy.providers",
20 | ],
21 | package_dir={"": "src"},
22 | description="Multitenancy package for Masonite!",
23 | long_description=long_description,
24 | long_description_content_type="text/markdown",
25 | # The project's main homepage.
26 | url="https://github.com/py-package/masonite-multitenancy",
27 | # Author details
28 | author="Yubaraj Shrestha",
29 | author_email="yubaraj@pypackage.com",
30 | # Choose your license
31 | license="MIT license",
32 | # If your package should include things you specify in your MANIFEST.in file
33 | # Use this option if your package needs to include files that are not python files
34 | # like html templates or css files
35 | include_package_data=True,
36 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
37 | classifiers=[
38 | # How mature is this project? Common values are
39 | # 3 - Alpha
40 | # 4 - Beta
41 | # 5 - Production/Stable
42 | "Development Status :: 5 - Production/Stable",
43 | # Indicate who your project is intended for
44 | "Intended Audience :: Developers",
45 | "Topic :: Software Development :: Build Tools",
46 | "Environment :: Web Environment",
47 | # Pick your license as you wish (should match "license" above)
48 | "License :: OSI Approved :: MIT License",
49 | "Operating System :: OS Independent",
50 | # Specify the Python versions you support here. In particular, ensure
51 | # that you indicate whether you support Python 2, Python 3 or both.
52 | "Programming Language :: Python :: 3.7",
53 | "Programming Language :: Python :: 3.8",
54 | "Programming Language :: Python :: 3.9",
55 | "Topic :: Internet :: WWW/HTTP",
56 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
57 | "Topic :: Internet :: WWW/HTTP :: WSGI",
58 | "Topic :: Software Development :: Libraries :: Application Frameworks",
59 | "Topic :: Software Development :: Libraries :: Python Modules",
60 | # List package on masonite packages website
61 | "Framework :: Masonite",
62 | ],
63 | # What does your project relate to?
64 | keywords="Masonite, Python, Development",
65 | # List run-time dependencies here. These will be installed by pip when
66 | # your project is installed. For an analysis of "install_requires" vs pip's
67 | # requirements files see:
68 | # https://packaging.python.org/en/latest/requirements.html
69 | install_requires=["masonite>=4.0,<5.0"],
70 | # List additional groups of dependencies here (e.g. development
71 | # dependencies). You can install these using the following syntax,
72 | # for example:
73 | # $ pip install -e .[dev,test]
74 | # $ pip install your-package[dev,test]
75 | extras_require={
76 | "dev": [
77 | "black",
78 | "flake8",
79 | "coverage",
80 | "pytest",
81 | "pytest-cov",
82 | "twine>=1.5.0",
83 | "wheel",
84 | ],
85 | },
86 | # If there are data files included in your packages that need to be
87 | # installed, specify them here. If using Python 2.6 or less, then these
88 | # have to be included in MANIFEST.in as well.
89 | package_data={
90 | # 'templates/index.html': [],
91 | },
92 | )
93 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/src/__init__.py
--------------------------------------------------------------------------------
/src/multitenancy/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa F401
2 | from .providers.multitenancy_provider import MultitenancyProvider
3 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyCreate.py:
--------------------------------------------------------------------------------
1 | from masonite.commands import Command
2 | from ..models.Tenant import Tenant
3 |
4 |
5 | class TenancyCreate(Command):
6 | """
7 | Creates a new tenant.
8 |
9 | tenancy:create
10 | """
11 |
12 | def __init__(self, application):
13 | super().__init__()
14 | self.app = application
15 |
16 | def handle(self):
17 | name = self.ask("Name: ")
18 | domain = self.ask("Domain: ")
19 | database = self.ask("Database: ")
20 |
21 | if not name:
22 | self.error("Name is required!")
23 | exit()
24 |
25 | if not domain:
26 | self.error("Domain is required!")
27 | exit()
28 |
29 | if not database:
30 | self.error("Database name is required!")
31 | exit()
32 |
33 | tenant = Tenant.where("domain", domain).or_where("database", database).first()
34 | if tenant:
35 | self.error("Tenant already exists!")
36 | exit()
37 |
38 | tenant = Tenant.create(name=name, domain=domain, database=database)
39 | self.info("Tenant created!")
40 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyDelete.py:
--------------------------------------------------------------------------------
1 | from masonite.commands import Command
2 | from ..facades import Tenancy
3 |
4 |
5 | class TenancyDelete(Command):
6 | """
7 | Delete all tenants or a specific tenant.
8 |
9 | tenancy:delete
10 | {--tenants=default : List of tenants to delete}
11 | """
12 |
13 | def __init__(self, application):
14 | super().__init__()
15 | self.app = application
16 |
17 | def get_tenants(self):
18 | """Returns a list of all tenants."""
19 | tenants = self.option("tenants")
20 | try:
21 | if tenants == "default":
22 | tenants = Tenancy.get_tenants()
23 | else:
24 | tenants = tenants.split(",")
25 | tenants = [Tenancy.get_tenant(tenant) for tenant in tenants]
26 | return tenants
27 | except Exception as e:
28 | self.error(e)
29 | return []
30 |
31 | def handle(self):
32 | tenants = self.get_tenants()
33 |
34 | if len(tenants) == 0:
35 | self.error("No tenants found!")
36 | exit()
37 |
38 | for tenant in tenants:
39 | Tenancy.delete(tenant)
40 |
41 | self.info("All tenants deleted!")
42 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyList.py:
--------------------------------------------------------------------------------
1 | from masonite.commands import Command
2 | from ..models.Tenant import Tenant
3 |
4 |
5 | class TenancyList(Command):
6 | """
7 | List all tenants.
8 |
9 | tenancy:list
10 | """
11 |
12 | def __init__(self, application):
13 | super().__init__()
14 | self.app = application
15 |
16 | def handle(self):
17 | tenants = Tenant.all()
18 |
19 | if len(tenants) == 0:
20 | self.error("No tenants found!")
21 | exit()
22 |
23 | self.info("=================Tenants List=================")
24 | for tenant in tenants:
25 | print("Tenant: " + tenant.name)
26 | print("Domain: " + tenant.domain)
27 | print("Database: " + tenant.database)
28 | print("----------------------------------------------")
29 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyMigrate.py:
--------------------------------------------------------------------------------
1 | import os
2 | from masonite.commands.Command import Command
3 | from masoniteorm.migrations import Migration
4 | from ..facades import Tenancy
5 |
6 |
7 | class TenancyMigrate(Command):
8 | """
9 | Migrates database to all tenants or to a specific tenant.
10 |
11 | tenancy:migrate
12 | {--tenants=default : List of tenants to migrate}
13 | {--m|migration=all : Migration's name to be migrated}
14 | {--f|force : Force migrations without prompt in production}
15 | {--s|show : Shows the output of SQL for migrations that would be running}
16 | {--d|directory=databases/migrations : The location of the migration directory}
17 | """
18 |
19 | def __init__(self, application):
20 | super().__init__()
21 | self.app = application
22 |
23 | def migration(self, tenant):
24 | Tenancy.set_connection(tenant)
25 |
26 | migration = Migration(
27 | command_class=self,
28 | connection="default",
29 | migration_directory=self.option("directory"),
30 | config_path=None,
31 | schema=None,
32 | )
33 |
34 | migration.create_table_if_not_exists()
35 | if not migration.get_unran_migrations():
36 | self.info(f"Nothing to migrate for tenant: {tenant.name}!")
37 | return
38 |
39 | return migration
40 |
41 | def handle(self):
42 | if os.getenv("APP_ENV") == "production" and not self.option("force"):
43 | answer = ""
44 | while answer not in ["y", "n"]:
45 | answer = input("Do you want to run migrations in PRODUCTION ? (y/n)\n").lower()
46 | if answer != "y":
47 | self.info("Migrations cancelled")
48 | exit(0)
49 |
50 | tenants = Tenancy.get_tenants(self.option("tenants"))
51 |
52 | if len(tenants) == 0:
53 | self.error("No tenants found!")
54 | exit()
55 |
56 | for tenant in tenants:
57 | migration = self.migration(tenant)
58 | if migration:
59 | migration_name = self.option("migration")
60 | show_output = self.option("show")
61 | self.info(f"Migrating tenant: {tenant.name}")
62 | self.warning("=====================START=====================")
63 | migration.migrate(migration=migration_name, output=show_output)
64 | self.warning("======================END======================")
65 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyMigrateRefresh.py:
--------------------------------------------------------------------------------
1 | from masonite.commands.Command import Command
2 | from masoniteorm.migrations import Migration
3 | from ..facades import Tenancy
4 |
5 |
6 | class TenancyMigrateRefresh(Command):
7 | """
8 | Refreshes database of all tenants or of a specific tenant.
9 |
10 | tenancy:migrate:refresh
11 | {--tenants=default : List of tenants to migrate}
12 | {--m|migration=all : Migration's name to be migrated}
13 | {--d|directory=databases/migrations : The location of the migration directory}
14 | """
15 |
16 | def __init__(self, application):
17 | super().__init__()
18 | self.app = application
19 |
20 | def migration(self, tenant):
21 | Tenancy.set_connection(tenant)
22 |
23 | return Migration(
24 | command_class=self,
25 | connection="default",
26 | migration_directory=self.option("directory"),
27 | config_path=None,
28 | schema=None,
29 | )
30 |
31 | def handle(self):
32 | tenants = Tenancy.get_tenants(self.option("tenants"))
33 |
34 | if len(tenants) == 0:
35 | self.error("No tenants found!")
36 | exit()
37 |
38 | for tenant in tenants:
39 | self.info(f"Refreshing tenant: {tenant.name}")
40 | self.warning("=====================START=====================")
41 | self.migration(tenant).refresh(self.option("migration"))
42 | self.warning("======================END======================")
43 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyMigrateReset.py:
--------------------------------------------------------------------------------
1 | from masonite.commands.Command import Command
2 | from masoniteorm.migrations import Migration
3 | from ..facades import Tenancy
4 |
5 |
6 | class TenancyMigrateReset(Command):
7 | """
8 | Resets migration of all tenants or of a specific tenant.
9 |
10 | tenancy:migrate:reset
11 | {--tenants=default : List of tenants to reset}
12 | {--m|migration=all : Migration's name to reset}
13 | {--d|directory=databases/migrations : The location of the migration directory}
14 | """
15 |
16 | def __init__(self, application):
17 | super().__init__()
18 | self.app = application
19 |
20 | def migration(self, tenant):
21 | Tenancy.set_connection(tenant)
22 |
23 | return Migration(
24 | command_class=self,
25 | connection="default",
26 | migration_directory=self.option("directory"),
27 | config_path=None,
28 | schema=None,
29 | )
30 |
31 | def handle(self):
32 | tenants = Tenancy.get_tenants(self.option("tenants"))
33 |
34 | if len(tenants) == 0:
35 | self.error("No tenants found!")
36 | exit()
37 |
38 | for tenant in tenants:
39 | self.info(f"Resetting tenant: {tenant.name}")
40 | self.warning("=====================START=====================")
41 | self.migration(tenant).reset(self.option("migration"))
42 | self.warning("======================END======================")
43 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyMigrateRollback.py:
--------------------------------------------------------------------------------
1 | from masonite.commands.Command import Command
2 | from masoniteorm.migrations import Migration
3 | from ..facades import Tenancy
4 |
5 |
6 | class TenancyMigrateRollback(Command):
7 | """
8 | Rolls back the last batch of migration of all tenants or of a specific tenant.
9 |
10 | tenancy:migrate:rollback
11 | {--tenants=default : List of tenants to reset}
12 | {--m|migration=all : Migration's name to reset}
13 | {--s|show : Shows the output of SQL for migrations that would be running}
14 | {--d|directory=databases/migrations : The location of the migration directory}
15 | """
16 |
17 | def __init__(self, application):
18 | super().__init__()
19 | self.app = application
20 |
21 | def migration(self, tenant):
22 | Tenancy.set_connection(tenant)
23 |
24 | return Migration(
25 | command_class=self,
26 | connection="default",
27 | migration_directory=self.option("directory"),
28 | config_path=None,
29 | schema=None,
30 | )
31 |
32 | def handle(self):
33 | tenants = Tenancy.get_tenants(self.option("tenants"))
34 |
35 | if len(tenants) == 0:
36 | self.error("No tenants found!")
37 | exit()
38 |
39 | for tenant in tenants:
40 | self.info(f"Rolling back tenant: {tenant.name}")
41 | self.warning("=====================START=====================")
42 | self.migration(tenant).rollback(
43 | migration=self.option("migration"), output=self.option("show")
44 | )
45 | self.warning("======================END======================")
46 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancyMigrateStatus.py:
--------------------------------------------------------------------------------
1 | from masonite.commands.Command import Command
2 | from masoniteorm.migrations import Migration
3 | from ..facades import Tenancy
4 |
5 |
6 | class TenancyMigrateStatus(Command):
7 | """
8 | Display migration status of all tenants or of a specific tenant.
9 |
10 | tenancy:migrate:status
11 | {--tenants=default : List of tenants}
12 | {--d|directory=databases/migrations : The location of the migration directory}
13 | """
14 |
15 | def __init__(self, application):
16 | super().__init__()
17 | self.app = application
18 |
19 | def migration(self, tenant):
20 | Tenancy.set_connection(tenant)
21 |
22 | migration = Migration(
23 | command_class=self,
24 | connection="default",
25 | migration_directory=self.option("directory"),
26 | config_path=None,
27 | schema=None,
28 | )
29 | migration.create_table_if_not_exists()
30 | table = self.table()
31 | table.set_header_row(["Ran?", "Migration"])
32 | migrations = []
33 |
34 | for migration_file in migration.get_ran_migrations():
35 | migrations.append(["Y ", f"{migration_file} "])
36 |
37 | for migration_file in migration.get_unran_migrations():
38 | migrations.append(["N ", f"{migration_file} "])
39 |
40 | table.set_rows(migrations)
41 |
42 | table.render(self.io)
43 |
44 | def handle(self):
45 | tenants = Tenancy.get_tenants(self.option("tenants"))
46 |
47 | if len(tenants) == 0:
48 | self.error("No tenants found!")
49 | exit()
50 |
51 | for tenant in tenants:
52 | self.info(f"Status of tenant: {tenant.name}")
53 | self.warning("=====================START=====================")
54 | self.migration(tenant)
55 | self.warning("======================END======================")
56 |
--------------------------------------------------------------------------------
/src/multitenancy/commands/TenancySeed.py:
--------------------------------------------------------------------------------
1 | from masonite.commands.Command import Command
2 | from masoniteorm.seeds import Seeder
3 | from inflection import camelize, underscore
4 | from ..facades import Tenancy
5 |
6 |
7 | class TenancySeed(Command):
8 | """
9 | Seed data to all tenants or to a specific tenant.
10 |
11 | tenancy:seed:run
12 | {--tenants=default : List of tenants to run seeder}
13 | {--dry : If the seed should run in dry mode}
14 | {table=None : Name of the table to seed}
15 | {--d|directory=databases/seeds : The location of the seed directory}
16 | """
17 |
18 | def __init__(self, application):
19 | super().__init__()
20 | self.app = application
21 |
22 | def seed(self, tenant):
23 | Tenancy.set_connection(tenant)
24 |
25 | seeder = Seeder(
26 | dry=self.option("dry"),
27 | seed_path=self.option("directory"),
28 | connection=tenant.database,
29 | )
30 |
31 | if self.argument("table") == "None":
32 | seeder.run_database_seed()
33 | seeder_seeded = "Database Seeder"
34 | else:
35 | table = self.argument("table")
36 | seeder_file = f"{underscore(table)}_table_seeder.{camelize(table)}TableSeeder"
37 | seeder.run_specific_seed(seeder_file)
38 | seeder_seeded = f"{camelize(table)}TableSeeder"
39 |
40 | self.line(f"{seeder_seeded} seeded! ")
41 |
42 | def handle(self):
43 | tenants = Tenancy.get_tenants(self.option("tenants"))
44 |
45 | if len(tenants) == 0:
46 | self.error("No tenants found!")
47 | exit()
48 |
49 | for tenant in tenants:
50 | self.info(f"Seeding tenant: {tenant.name}")
51 | self.warning("=====================START=====================")
52 | self.seed(tenant)
53 | self.warning("======================END======================")
54 |
--------------------------------------------------------------------------------
/src/multitenancy/config/multitenancy.py:
--------------------------------------------------------------------------------
1 | """multitenancy Settings"""
2 |
3 | """
4 | |--------------------------------------------------------------------------
5 | | MultiTenancy
6 | |--------------------------------------------------------------------------
7 | |
8 | | Multitenancy is a feature that allows you to have multiple tenants in your
9 | | application. This is useful for things like a company having multiple
10 | | websites, or a company having multiple apps.
11 | |
12 | """
13 |
14 | TENANTS = {}
15 |
--------------------------------------------------------------------------------
/src/multitenancy/contexts/TenantContext.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa F501
2 | from typing import Optional
3 | from ..facades import Tenancy
4 | from ..models.Tenant import Tenant
5 | import subprocess
6 | from masonite.environment import env
7 |
8 |
9 | class TenantContext(object):
10 | def __init__(self, tenant: Optional[Tenant] = None):
11 | if not tenant:
12 | raise Exception("Tenant is required")
13 |
14 | if not isinstance(tenant, Tenant):
15 | raise Exception("`tenant` must be an instance of Tenant")
16 |
17 | self.tenant = tenant
18 |
19 | self.migration_dir = env("DB_MIGRATIONS_DIR", "databases/migrations")
20 | self.seeders_dir = env("DB_SEEDERS_DIR", "databases/seeds")
21 |
22 | def __enter__(self):
23 | Tenancy.set_connection(self.tenant)
24 | return self
25 |
26 | def __exit__(self, exception_type, exception_value, traceback):
27 | Tenancy.reset_connection()
28 |
29 | def migrate(self):
30 | # executes migrations for tenant
31 |
32 | command = f"python craft tenancy:migrate --tenants={self.tenant.database} --d={self.migration_dir}"
33 |
34 | process = subprocess.Popen(
35 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
36 | )
37 | output, error = process.communicate()
38 |
39 | if error:
40 | print(error)
41 | return output
42 |
43 | def migrate_refresh(self):
44 | # refreshes database of a tenant
45 |
46 | command = f"python craft tenancy:migrate:refresh --tenants={self.tenant.database} --d={self.migration_dir}"
47 |
48 | process = subprocess.Popen(
49 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
50 | )
51 | output, error = process.communicate()
52 |
53 | if error:
54 | print(error)
55 | return output
56 |
57 | def migrate_rollback(self):
58 | # rolls back migration changes in database of a tenant
59 |
60 | command = f"python craft tenancy:migrate:rollback --tenants={self.tenant.database} --d={self.migration_dir}"
61 |
62 | process = subprocess.Popen(
63 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
64 | )
65 | output, error = process.communicate()
66 |
67 | if error:
68 | print(error)
69 | return output
70 |
71 | def migrate_reset(self):
72 | # Resets the database of a tenant
73 |
74 | command = f"python craft tenancy:migrate:reset --tenants={self.tenant.database} --d={self.migration_dir}"
75 |
76 | process = subprocess.Popen(
77 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
78 | )
79 | output, error = process.communicate()
80 |
81 | if error:
82 | print(error)
83 | return output
84 |
85 | def migrate_status(self):
86 | # displays migration status of a tenant
87 |
88 | command = f"python craft tenancy:migrate:status --tenants={self.tenant.database} --d={self.migration_dir}"
89 |
90 | process = subprocess.Popen(
91 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
92 | )
93 | output, error = process.communicate()
94 |
95 | if error:
96 | print(error)
97 | return output
98 |
99 | def seed(self):
100 | # seeds data into database of a tenant
101 |
102 | command = f"python craft tenancy:seed:run --tenants={self.tenant.database} --d={self.seeders_dir}"
103 |
104 | process = subprocess.Popen(
105 | [command], stdout=subprocess.PIPE, shell=True, universal_newlines=True
106 | )
107 | output, error = process.communicate()
108 |
109 | if error:
110 | print(error)
111 | return output
112 |
--------------------------------------------------------------------------------
/src/multitenancy/contexts/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa F401
2 | from .TenantContext import TenantContext
3 |
--------------------------------------------------------------------------------
/src/multitenancy/facades/Tenancy.py:
--------------------------------------------------------------------------------
1 | from masonite.facades import Facade
2 |
3 |
4 | class Tenancy(metaclass=Facade):
5 | key = "multitenancy"
6 |
--------------------------------------------------------------------------------
/src/multitenancy/facades/Tenancy.pyi:
--------------------------------------------------------------------------------
1 | from masonite.request.request import Request
2 | from ..models.Tenant import Tenant
3 |
4 | class Tenancy:
5 | def reset_connection() -> None:
6 | """Resets the connection to the default connection."""
7 | ...
8 | def set_connection(tenant: Tenant) -> None:
9 | """Sets the connection for the tenant."""
10 | ...
11 | def get_tenants() -> list[Tenant]:
12 | """Returns a list of all tenants."""
13 | ...
14 | def find_tenant(request: Request) -> Tenant | None:
15 | """Returns the tenant for the current request."""
16 | ...
17 | def delete(tenant: Tenant) -> None:
18 | """Deletes a tenant."""
19 | ...
20 | def create(name: str, domain: str, database: str) -> Tenant:
21 | """Creates a new tenant."""
22 | ...
23 | def get_tenant_by_domain(domain: str) -> Tenant | None:
24 | """Returns a tenant by domain."""
25 | ...
26 | def get_tenant_by_database(database: str) -> Tenant | None:
27 | """Returns a tenant by database."""
28 | ...
29 | def get_tenant_by_id(id: int) -> Tenant | None:
30 | """Returns a tenant by id."""
31 | ...
32 |
--------------------------------------------------------------------------------
/src/multitenancy/facades/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa F401
2 | from .Tenancy import Tenancy
3 |
--------------------------------------------------------------------------------
/src/multitenancy/middlewares/tenant_finder_middleware.py:
--------------------------------------------------------------------------------
1 | from masonite.middleware import Middleware
2 | from ..facades import Tenancy
3 |
4 |
5 | class TenantFinderMiddleware(Middleware):
6 | """Middleware to find the tenant for the current request."""
7 |
8 | def before(self, request, response):
9 | """Find the tenant for the current request."""
10 | tenant = Tenancy.find_tenant(request)
11 | if tenant is not None:
12 | Tenancy.use_tenant(tenant)
13 | request.tenant = tenant
14 | return request
15 |
16 | def after(self, request, response):
17 | """Return the response."""
18 | return response
19 |
--------------------------------------------------------------------------------
/src/multitenancy/migrations/2022_07_07_230413_create_tenants_table.py:
--------------------------------------------------------------------------------
1 | """CreateTenantsTable Migration."""
2 |
3 | from masoniteorm.migrations import Migration
4 |
5 |
6 | class CreateTenantsTable(Migration):
7 | def up(self):
8 | """
9 | Run the migrations.
10 | """
11 | with self.schema.create("tenants") as table:
12 | table.increments("id")
13 | table.string("name")
14 | table.string("domain").unique()
15 | table.string("database").unique()
16 | table.boolean("status").default(False)
17 | table.timestamps()
18 |
19 | def down(self):
20 | """
21 | Revert the migrations.
22 | """
23 | self.schema.drop("tenants")
24 |
--------------------------------------------------------------------------------
/src/multitenancy/models/Tenant.py:
--------------------------------------------------------------------------------
1 | """Tenant Model."""
2 | from masoniteorm.models import Model
3 |
4 |
5 | class Tenant(Model):
6 | """Tenant Model."""
7 |
8 | __fillable__ = ["name", "domain", "database"]
9 |
--------------------------------------------------------------------------------
/src/multitenancy/multitenancy.py:
--------------------------------------------------------------------------------
1 | from masonite.request.request import Request
2 | from .models.Tenant import Tenant
3 | from masonite.configuration import config
4 | from masoniteorm.connections import ConnectionResolver
5 |
6 |
7 | class MultiTenancy:
8 | def __init__(self, application):
9 | self.app = application
10 | self.tenant_configs = config("multitenancy.tenants")
11 |
12 | def get_tenants(self, tenants):
13 | """Returns a list of all tenants."""
14 | try:
15 | if tenants == "default":
16 | tenants = self.__all()
17 | else:
18 | tenants = tenants.split(",")
19 | tenants = [self.__one(tenant) for tenant in tenants]
20 | return tenants
21 | except Exception as e:
22 | print(e)
23 | return []
24 |
25 | def __all(self):
26 | """Returns a list of all tenants."""
27 | return Tenant.all()
28 |
29 | def __one(self, tenant_name):
30 | """Returns a tenant by name."""
31 | tenant = Tenant.where("database", tenant_name).first()
32 | if not tenant:
33 | raise Exception(f"Tenant: `{tenant_name}` not found!")
34 | return tenant
35 |
36 | def set_connection(self, tenant):
37 | """Sets the connection for the tenant."""
38 | resolver = ConnectionResolver()
39 | connections = resolver.get_connection_details().copy()
40 | new_connections = {}
41 | for key, value in connections.items():
42 | if key == "default":
43 | new_connections[key] = value
44 | else:
45 | new_connections[key] = value.copy()
46 | if connections["default"] == "sqlite":
47 | new_connections[key]["database"] = f"{tenant.database}.sqlite3"
48 | else:
49 | new_connections[key]["database"] = tenant.database
50 |
51 | ConnectionResolver().set_connection_details(new_connections)
52 |
53 | def delete(self, tenant):
54 | """Deletes a tenant."""
55 | tenant.delete()
56 |
57 | def get_subdomain(self, request: Request):
58 | """Returns the subdomain for the current request."""
59 | hosts = [request.environ.get("HTTP_HOST"), request.environ.get("HTTP_X_FORWARDED_HOST")]
60 | subdomains = []
61 | for host in hosts:
62 | if host:
63 | items = host.split(".")
64 | if len(items) > 2:
65 | subdomains.append(items[0])
66 | return subdomains, hosts
67 |
68 | def reset_connection(self):
69 | resolver = ConnectionResolver()
70 | connections = resolver.get_connection_details()
71 | database_config = config("database.databases")
72 | current_db = connections.get(connections.get("default"))["database"]
73 | original_db = database_config.get(connections.get("default"))["database"]
74 |
75 | if current_db != original_db:
76 | ConnectionResolver().set_connection_details(database_config)
77 |
78 | def use_tenant(self, tenant):
79 | """Sets the tenant for the current request."""
80 | self.reset_connection()
81 | self.set_connection(tenant)
82 |
83 | def find_tenant(self, request: Request) -> Tenant:
84 | """Finds the tenant for the current request."""
85 | subdomains, hosts = self.get_subdomain(request)
86 | subdomains = tuple(subdomains)
87 | hosts = tuple(hosts)
88 | try:
89 | self.reset_connection()
90 | return Tenant.where_raw("database in {database}".format(database=subdomains)).first()
91 | except Exception:
92 | return None
93 |
94 | def create(self, name: str, database: str, domain: str):
95 | """Creates a new tenant."""
96 |
97 | tenant = Tenant.where("domain", domain).or_where("database", database).first()
98 | if tenant:
99 | raise Exception(
100 | f"Tenant: Domain: `{domain}` or Database: `{database}` already exists!"
101 | )
102 |
103 | tenant = Tenant.create(name=name, domain=domain, database=database)
104 | return tenant
105 |
106 | def get_tenant_by_domain(self, domain: str):
107 | """Returns a tenant by domain."""
108 |
109 | return Tenant.where("domain", domain).first()
110 |
111 | def get_tenant_by_database(self, database: str):
112 | """Returns a tenant by database."""
113 |
114 | return Tenant.where("database", database).first()
115 |
116 | def get_tenant_by_id(self, id: int):
117 | """Returns a tenant by id."""
118 |
119 | return Tenant.find(id)
120 |
--------------------------------------------------------------------------------
/src/multitenancy/providers/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa: E501
2 | from .multitenancy_provider import MultitenancyProvider
3 |
--------------------------------------------------------------------------------
/src/multitenancy/providers/multitenancy_provider.py:
--------------------------------------------------------------------------------
1 | """A MultitenancyProvider Service Provider."""
2 |
3 | from masonite.packages import PackageProvider
4 |
5 | from ..middlewares.tenant_finder_middleware import TenantFinderMiddleware
6 | from ..commands.TenancyMigrate import TenancyMigrate
7 | from ..commands.TenancyCreate import TenancyCreate
8 | from ..commands.TenancyList import TenancyList
9 | from ..commands.TenancyDelete import TenancyDelete
10 | from ..commands.TenancyMigrateRefresh import TenancyMigrateRefresh
11 | from ..commands.TenancyMigrateRollback import TenancyMigrateRollback
12 | from ..commands.TenancyMigrateReset import TenancyMigrateReset
13 | from ..commands.TenancyMigrateStatus import TenancyMigrateStatus
14 | from ..commands.TenancySeed import TenancySeed
15 | from ..multitenancy import MultiTenancy
16 |
17 |
18 | class MultitenancyProvider(PackageProvider):
19 | def configure(self):
20 | """Register objects into the Service Container."""
21 | (
22 | self.root("multitenancy")
23 | .name("multitenancy")
24 | .config("config/multitenancy.py", publish=True)
25 | )
26 |
27 | def register(self):
28 | super().register()
29 | self.application.bind("multitenancy", MultiTenancy(self.application))
30 | self.application.make("middleware").add({"multitenancy": [TenantFinderMiddleware]})
31 | (
32 | self.application.make("commands")
33 | .add(TenancyMigrate(self.application))
34 | .add(TenancyCreate(self.application))
35 | .add(TenancyList(self.application))
36 | .add(TenancyDelete(self.application))
37 | .add(TenancyMigrateRefresh(self.application))
38 | .add(TenancyMigrateRollback(self.application))
39 | .add(TenancyMigrateStatus(self.application))
40 | .add(TenancyMigrateReset(self.application))
41 | .add(TenancySeed(self.application))
42 | )
43 |
44 | def boot(self):
45 | """Boots services required by the container."""
46 | pass
47 |
--------------------------------------------------------------------------------
/tenant1.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tenant1.sqlite3
--------------------------------------------------------------------------------
/tenant2.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tenant2.sqlite3
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/Kernel.py:
--------------------------------------------------------------------------------
1 | from masonite.foundation import response_handler
2 | from masonite.storage import StorageCapsule
3 | from masonite.auth import Sign
4 | from masonite.environment import LoadEnvironment
5 | from masonite.utils.structures import load
6 | from masonite.utils.location import base_path
7 | from masonite.middleware import (
8 | SessionMiddleware,
9 | EncryptCookies,
10 | LoadUserMiddleware,
11 | MaintenanceModeMiddleware,
12 | )
13 | from masonite.routes import Route
14 | from masonite.configuration.Configuration import Configuration
15 | from masonite.configuration import config
16 |
17 | from tests.integrations.app.middlewares import VerifyCsrfToken, AuthenticationMiddleware
18 |
19 |
20 | class Kernel:
21 |
22 | http_middleware = [MaintenanceModeMiddleware, EncryptCookies]
23 |
24 | route_middleware = {
25 | "web": [SessionMiddleware, LoadUserMiddleware, VerifyCsrfToken],
26 | "auth": [AuthenticationMiddleware],
27 | }
28 |
29 | def __init__(self, app):
30 | self.application = app
31 |
32 | def register(self):
33 | # Register routes
34 | self.load_environment()
35 | self.register_configurations()
36 | self.register_middleware()
37 | self.register_routes()
38 | self.register_database()
39 | self.register_templates()
40 | self.register_storage()
41 |
42 | def load_environment(self):
43 | LoadEnvironment()
44 |
45 | def register_configurations(self):
46 | # load configuration
47 | self.application.bind("config.location", "tests/integrations/config")
48 | configuration = Configuration(self.application)
49 | configuration.load()
50 | self.application.bind("config", configuration)
51 | key = config("application.key")
52 | self.application.bind("key", key)
53 | self.application.bind("sign", Sign(key))
54 | # set locations
55 | self.application.bind("resources.location", "tests/integrations/resources/")
56 | self.application.bind("controllers.location", "tests/integrations/app/controllers")
57 | self.application.bind("jobs.location", "tests/integrations/app/jobs")
58 | self.application.bind("providers.location", "tests/integrations/app/providers")
59 | self.application.bind("mailables.location", "tests/integrations/app/mailables")
60 | self.application.bind("listeners.location", "tests/integrations/app/listeners")
61 | self.application.bind("validation.location", "tests/integrations/app/validation")
62 | self.application.bind("notifications.location", "tests/integrations/app/notifications")
63 | self.application.bind("events.location", "tests/integrations/app/events")
64 | self.application.bind("tasks.location", "tests/integrations/app/tasks")
65 | self.application.bind("models.location", "tests/integrations/app/models")
66 | self.application.bind("observers.location", "tests/integrations/app/models/observers")
67 | self.application.bind("policies.location", "tests/integrations/app/policies")
68 | self.application.bind("commands.location", "tests/integrations/app/commands")
69 | self.application.bind("middlewares.location", "tests/integrations/app/middlewares")
70 |
71 | self.application.bind("server.runner", "masonite.commands.ServeCommand.main")
72 |
73 | def register_middleware(self):
74 | self.application.make("middleware").add(self.route_middleware).add(self.http_middleware)
75 |
76 | def register_routes(self):
77 | Route.set_controller_locations(self.application.make("controllers.location"))
78 | self.application.bind("routes.location", "tests/integrations/routes/web")
79 | self.application.make("router").add(
80 | Route.group(
81 | load(self.application.make("routes.location"), "ROUTES"), middleware=["web"]
82 | )
83 | )
84 |
85 | def register_database(self):
86 | from masoniteorm.query import QueryBuilder
87 |
88 | self.application.bind(
89 | "builder",
90 | QueryBuilder(connection_details=config("database.databases")),
91 | )
92 |
93 | self.application.bind("migrations.location", "tests/integrations/databases/migrations")
94 | self.application.bind("seeds.location", "tests/integrations/databases/seeds")
95 |
96 | self.application.bind("resolver", config("database.db"))
97 |
98 | def register_templates(self):
99 | self.application.bind("views.location", "tests/integrations/templates/")
100 |
101 | def register_storage(self):
102 | storage = StorageCapsule()
103 | storage.add_storage_assets(config("filesystem.staticfiles"))
104 | self.application.bind("storage_capsule", storage)
105 |
106 | self.application.set_response_handler(response_handler)
107 | self.application.use_storage_path(base_path("storage"))
108 |
--------------------------------------------------------------------------------
/tests/integrations/app/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/app/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/app/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/app/commands/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/app/controllers/WelcomeController.py:
--------------------------------------------------------------------------------
1 | """A WelcomeController Module."""
2 | from masonite.views import View
3 | from masonite.controllers import Controller
4 | from tests.integrations.app.models.User import User
5 |
6 |
7 | class WelcomeController(Controller):
8 | """WelcomeController Controller Class."""
9 |
10 | def show(self, view: View):
11 | return view.render("welcome")
12 |
13 | def index(self):
14 | User.create({"name": "John Doe", "email": "john@doe.com", "password": "capslock"})
15 |
--------------------------------------------------------------------------------
/tests/integrations/app/controllers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/app/controllers/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/app/middlewares/AuthenticationMiddleware.py:
--------------------------------------------------------------------------------
1 | from masonite.middleware import Middleware
2 |
3 |
4 | class AuthenticationMiddleware(Middleware):
5 | """Middleware to check if the user is logged in."""
6 |
7 | def before(self, request, response):
8 | if not request.user():
9 | return response.redirect(name="login")
10 | return request
11 |
12 | def after(self, request, response):
13 | return request
14 |
--------------------------------------------------------------------------------
/tests/integrations/app/middlewares/VerifyCsrfToken.py:
--------------------------------------------------------------------------------
1 | from masonite.middleware import VerifyCsrfToken as Middleware
2 |
3 |
4 | class VerifyCsrfToken(Middleware):
5 |
6 | exempt = []
7 |
--------------------------------------------------------------------------------
/tests/integrations/app/middlewares/__init__.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa: F401
2 | from .VerifyCsrfToken import VerifyCsrfToken
3 | from .AuthenticationMiddleware import AuthenticationMiddleware
4 |
--------------------------------------------------------------------------------
/tests/integrations/app/models/User.py:
--------------------------------------------------------------------------------
1 | """User Model."""
2 | from masoniteorm.models import Model
3 | from masoniteorm.scopes import SoftDeletesMixin
4 | from masonite.authentication import Authenticates
5 |
6 |
7 | class User(Model, SoftDeletesMixin, Authenticates):
8 | """User Model."""
9 |
10 | __fillable__ = ["name", "email", "password"]
11 | __hidden__ = ["password"]
12 | __auth__ = "email"
13 |
--------------------------------------------------------------------------------
/tests/integrations/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/config/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/config/application.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import env
2 |
3 |
4 | KEY = env("APP_KEY", "-RkDOqXojJIlsF_I8wWiUq_KRZ0PtGWTOZ676u5HtLg=")
5 |
6 | HASHING = {
7 | "default": env("HASHING_FUNCTION", "bcrypt"),
8 | "bcrypt": {"rounds": 10},
9 | "argon2": {"memory": 1024, "threads": 2, "time": 2},
10 | }
11 |
12 | APP_URL = env("APP_URL", "http://localhost:8000/")
13 |
--------------------------------------------------------------------------------
/tests/integrations/config/auth.py:
--------------------------------------------------------------------------------
1 | from tests.integrations.app.models.User import User
2 |
3 | GUARDS = {
4 | "default": "web",
5 | "web": {"model": User},
6 | "password_reset_table": "password_resets",
7 | "password_reset_expiration": 1440, # in minutes. 24 hours. None if disabled
8 | }
9 |
--------------------------------------------------------------------------------
/tests/integrations/config/broadcast.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import env
2 |
3 |
4 | BROADCASTS = {
5 | "default": "pusher",
6 | "pusher": {
7 | "driver": "pusher",
8 | "client": env("PUSHER_CLIENT"),
9 | "app_id": env("PUSHER_APP_ID"),
10 | "secret": env("PUSHER_SECRET"),
11 | "cluster": env("PUSHER_CLUSTER"),
12 | "ssl": False,
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/tests/integrations/config/cache.py:
--------------------------------------------------------------------------------
1 | # from masonite.environment import env
2 |
3 |
4 | STORES = {
5 | "default": "local",
6 | "local": {
7 | "driver": "file",
8 | "location": "storage/framework/cache"
9 | #
10 | },
11 | "redis": {
12 | "driver": "redis",
13 | "host": "127.0.0.1",
14 | "port": "6379",
15 | "password": "",
16 | "name": "project_name",
17 | },
18 | "memcache": {
19 | "driver": "memcache",
20 | "host": "127.0.0.1",
21 | "port": "11211",
22 | "password": "",
23 | "name": "project_name",
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/tests/integrations/config/database.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import LoadEnvironment, env
2 | from masoniteorm.connections import ConnectionResolver
3 |
4 | # Loads in the environment variables when this page is imported.
5 | LoadEnvironment()
6 |
7 | """
8 | The connections here don't determine the database but determine the "connection".
9 | They can be named whatever you want.
10 | """
11 | DATABASES = {
12 | "default": env("DB_CONNECTION", "sqlite"),
13 | "sqlite": {
14 | "driver": "sqlite",
15 | "database": env("SQLITE_DB_DATABASE", "masonite.sqlite3"),
16 | "prefix": "",
17 | "log_queries": env("DB_LOG"),
18 | },
19 | "mysql": {
20 | "driver": "mysql",
21 | "host": env("DB_HOST"),
22 | "user": env("DB_USERNAME"),
23 | "password": env("DB_PASSWORD"),
24 | "database": env("DB_DATABASE"),
25 | "port": env("DB_PORT"),
26 | "prefix": "",
27 | "grammar": "mysql",
28 | "options": {
29 | "charset": "utf8mb4",
30 | },
31 | "log_queries": env("DB_LOG"),
32 | },
33 | "postgres": {
34 | "driver": "postgres",
35 | "host": env("DB_HOST"),
36 | "user": env("DB_USERNAME"),
37 | "password": env("DB_PASSWORD"),
38 | "database": env("DB_DATABASE"),
39 | "port": env("DB_PORT"),
40 | "prefix": "",
41 | "grammar": "postgres",
42 | "log_queries": env("DB_LOG"),
43 | },
44 | "mssql": {
45 | "driver": "mssql",
46 | "host": env("MSSQL_DATABASE_HOST"),
47 | "user": env("MSSQL_DATABASE_USER"),
48 | "password": env("MSSQL_DATABASE_PASSWORD"),
49 | "database": env("MSSQL_DATABASE_DATABASE"),
50 | "port": env("MSSQL_DATABASE_PORT"),
51 | "prefix": "",
52 | "log_queries": env("DB_LOG"),
53 | },
54 | }
55 |
56 | DB = ConnectionResolver().set_connection_details(DATABASES)
57 |
--------------------------------------------------------------------------------
/tests/integrations/config/exceptions.py:
--------------------------------------------------------------------------------
1 | HANDLERS = {"stack_overflow": True, "solutions": True}
2 |
--------------------------------------------------------------------------------
/tests/integrations/config/filesystem.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import env
2 | from masonite.utils.location import base_path
3 |
4 |
5 | DISKS = {
6 | "default": "local",
7 | "local": {
8 | "driver": "file",
9 | "path": base_path("tests/integrations/storage/framework/filesystem"),
10 | },
11 | "s3": {
12 | "driver": "s3",
13 | "client": env("AWS_CLIENT"),
14 | "secret": env("AWS_SECRET"),
15 | "bucket": env("AWS_BUCKET"),
16 | },
17 | }
18 |
19 | STATICFILES = {
20 | # folder # template alias
21 | "tests/integrations/storage/static": "static/",
22 | "tests/integrations/storage/compiled": "assets/",
23 | "tests/integrations/storage/public": "/",
24 | }
25 |
--------------------------------------------------------------------------------
/tests/integrations/config/mail.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import env
2 |
3 |
4 | FROM_EMAIL = env("MAIL_FROM", "no-reply@masonite.com")
5 |
6 | DRIVERS = {
7 | "default": env("MAIL_DRIVER", "terminal"),
8 | "smtp": {
9 | "host": env("MAIL_HOST"),
10 | "port": env("MAIL_PORT"),
11 | "username": env("MAIL_USERNAME"),
12 | "password": env("MAIL_PASSWORD"),
13 | "from": FROM_EMAIL,
14 | },
15 | "mailgun": {
16 | "domain": env("MAILGUN_DOMAIN"),
17 | "secret": env("MAILGUN_SECRET"),
18 | "from": FROM_EMAIL,
19 | },
20 | "terminal": {
21 | "from": FROM_EMAIL,
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/tests/integrations/config/multitenancy.py:
--------------------------------------------------------------------------------
1 | """multitenancy Settings"""
2 |
3 | from masonite.environment import env
4 |
5 | # Loads in the environment variables when this page is imported.
6 |
7 | """
8 | |--------------------------------------------------------------------------
9 | | MultiTenancy
10 | |--------------------------------------------------------------------------
11 | |
12 | | Multitenancy is a feature that allows you to have multiple tenants in your
13 | | application. This is useful for things like a company having multiple
14 | | websites, or a company having multiple apps.
15 | |
16 | """
17 |
18 | TENANTS = {
19 | "tenant1": {
20 | "driver": "sqlite",
21 | "database": env("SQLITE_DB_DATABASE", "tenant1.sqlite3"),
22 | "prefix": "",
23 | "log_queries": env("DB_LOG"),
24 | },
25 | "tenant2": {
26 | "driver": "sqlite",
27 | "database": env("SQLITE_DB_DATABASE", "tenant2.sqlite3"),
28 | "prefix": "",
29 | "log_queries": env("DB_LOG"),
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/tests/integrations/config/notification.py:
--------------------------------------------------------------------------------
1 | from masonite.environment import env
2 |
3 |
4 | DRIVERS = {
5 | "slack": {
6 | "token": env("SLACK_TOKEN", ""), # used for API mode
7 | "webhook": env("SLACK_WEBHOOK", ""), # used for webhook mode
8 | },
9 | "vonage": {
10 | "key": env("VONAGE_KEY", ""),
11 | "secret": env("VONAGE_SECRET", ""),
12 | "sms_from": env("VONAGE_SMS_FROM", "+33000000000"),
13 | },
14 | "database": {
15 | "connection": "sqlite",
16 | "table": "notifications",
17 | },
18 | }
19 |
20 | DRY = False
21 |
--------------------------------------------------------------------------------
/tests/integrations/config/providers.py:
--------------------------------------------------------------------------------
1 | from masonite.providers import (
2 | RouteProvider,
3 | FrameworkProvider,
4 | ViewProvider,
5 | WhitenoiseProvider,
6 | ExceptionProvider,
7 | MailProvider,
8 | SessionProvider,
9 | QueueProvider,
10 | CacheProvider,
11 | EventProvider,
12 | StorageProvider,
13 | HelpersProvider,
14 | BroadcastProvider,
15 | AuthenticationProvider,
16 | AuthorizationProvider,
17 | HashServiceProvider,
18 | ORMProvider,
19 | )
20 |
21 |
22 | from masonite.scheduling.providers import ScheduleProvider
23 | from masonite.notification.providers import NotificationProvider
24 | from masonite.validation.providers import ValidationProvider
25 |
26 | # register local package
27 | from src.multitenancy import MultitenancyProvider
28 |
29 |
30 | PROVIDERS = [
31 | FrameworkProvider,
32 | HelpersProvider,
33 | RouteProvider,
34 | ViewProvider,
35 | WhitenoiseProvider,
36 | ExceptionProvider,
37 | MailProvider,
38 | NotificationProvider,
39 | SessionProvider,
40 | CacheProvider,
41 | QueueProvider,
42 | ScheduleProvider,
43 | EventProvider,
44 | StorageProvider,
45 | BroadcastProvider,
46 | HashServiceProvider,
47 | AuthenticationProvider,
48 | ValidationProvider,
49 | AuthorizationProvider,
50 | ORMProvider,
51 | ]
52 |
53 | PROVIDERS += [MultitenancyProvider]
54 |
--------------------------------------------------------------------------------
/tests/integrations/config/queue.py:
--------------------------------------------------------------------------------
1 | # from masonite.environment import env
2 |
3 |
4 | DRIVERS = {
5 | "default": "async",
6 | "database": {
7 | "connection": "sqlite",
8 | "table": "jobs",
9 | "failed_table": "failed_jobs",
10 | "attempts": 3,
11 | "poll": 5,
12 | },
13 | "redis": {
14 | #
15 | },
16 | "amqp": {
17 | "username": "guest",
18 | "password": "guest",
19 | "port": "5672",
20 | "vhost": "",
21 | "host": "localhost",
22 | "channel": "default",
23 | "queue": "masonite4",
24 | },
25 | "async": {
26 | "blocking": True,
27 | "callback": "handle",
28 | "mode": "threading",
29 | "workers": 1,
30 | },
31 | }
32 |
--------------------------------------------------------------------------------
/tests/integrations/config/session.py:
--------------------------------------------------------------------------------
1 | # from masonite.environment import env
2 |
3 |
4 | DRIVERS = {
5 | "default": "cookie",
6 | "cookie": {},
7 | }
8 |
--------------------------------------------------------------------------------
/tests/integrations/databases/migrations/2021_01_09_033202_create_password_reset_table.py:
--------------------------------------------------------------------------------
1 | from masoniteorm.migrations import Migration
2 |
3 |
4 | class CreatePasswordResetTable(Migration):
5 | def up(self):
6 | """Run the migrations."""
7 | with self.schema.create("password_resets") as table:
8 | table.string("email").unique()
9 | table.string("token")
10 | table.datetime("expires_at").nullable()
11 | table.datetime("created_at")
12 |
13 | def down(self):
14 | """Revert the migrations."""
15 | self.schema.drop("password_resets")
16 |
--------------------------------------------------------------------------------
/tests/integrations/databases/migrations/2021_01_09_043202_create_users_table.py:
--------------------------------------------------------------------------------
1 | from masoniteorm.migrations import Migration
2 |
3 |
4 | class CreateUsersTable(Migration):
5 | def up(self):
6 | """Run the migrations."""
7 | with self.schema.create("users") as table:
8 | table.increments("id")
9 | table.string("name")
10 | table.string("email").unique()
11 | table.string("password")
12 | table.string("second_password").nullable()
13 | table.string("remember_token").nullable()
14 | table.string("phone").nullable()
15 | table.timestamp("verified_at").nullable()
16 | table.timestamps()
17 | table.soft_deletes()
18 |
19 | def down(self):
20 | """Revert the migrations."""
21 | self.schema.drop("users")
22 |
--------------------------------------------------------------------------------
/tests/integrations/databases/migrations/2022_07_07_230413_create_tenants_table.py:
--------------------------------------------------------------------------------
1 | """CreateTenantsTable Migration."""
2 |
3 | from masoniteorm.migrations import Migration
4 |
5 |
6 | class CreateTenantsTable(Migration):
7 | def up(self):
8 | """
9 | Run the migrations.
10 | """
11 | with self.schema.create("tenants") as table:
12 | table.increments("id")
13 | table.string("name")
14 | table.string("domain").unique()
15 | table.string("database").unique()
16 | table.boolean("status").default(False)
17 | table.timestamps()
18 |
19 | def down(self):
20 | """
21 | Revert the migrations.
22 | """
23 | self.schema.drop("tenants")
24 |
--------------------------------------------------------------------------------
/tests/integrations/databases/seeds/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/databases/seeds/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/databases/seeds/database_seeder.py:
--------------------------------------------------------------------------------
1 | """Base Database Seeder Module."""
2 | from masoniteorm.seeds import Seeder
3 |
4 | from .user_table_seeder import UserTableSeeder
5 |
6 |
7 | class DatabaseSeeder(Seeder):
8 | def run(self):
9 | """Run the database seeds."""
10 | self.call(UserTableSeeder)
11 |
--------------------------------------------------------------------------------
/tests/integrations/databases/seeds/user_table_seeder.py:
--------------------------------------------------------------------------------
1 | """UserTableSeeder Seeder."""
2 | from masoniteorm.seeds import Seeder
3 | from masonite.facades import Hash
4 |
5 | from tests.integrations.app.models.User import User
6 |
7 |
8 | class UserTableSeeder(Seeder):
9 | def run(self):
10 | """Run the database seeds."""
11 | User.create(
12 | {
13 | "name": "Joe",
14 | "email": "user@example.com",
15 | "password": Hash.make("secret"),
16 | "phone": "+123456789",
17 | }
18 | )
19 |
--------------------------------------------------------------------------------
/tests/integrations/resources/css/app.css:
--------------------------------------------------------------------------------
1 | /* Put your CSS here */
2 |
--------------------------------------------------------------------------------
/tests/integrations/resources/js/app.js:
--------------------------------------------------------------------------------
1 |
2 | require("./bootstrap.js")
3 |
--------------------------------------------------------------------------------
/tests/integrations/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * We'll load the axios HTTP library which allows us to easily issue requests
3 | * to our Masonite back-end. This library automatically handles sending the
4 | * CSRF token as a header based on the value of the "XSRF" token cookie.
5 | */
6 |
7 | window.axios = require('axios');
8 |
9 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
10 |
11 | /**
12 | * Next we will register the CSRF Token as a common header with Axios so that
13 | * all outgoing HTTP requests automatically have it attached. This is just
14 | * a simple convenience so we don't have to attach every token manually.
15 | */
16 |
17 | let token = document.head.querySelector('meta[name="csrf-token"]');
18 |
19 | if (token) {
20 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
21 | } else {
22 | console.error('CSRF token not found: https://docs.masoniteproject.com/features/csrf#ajax-vue-axios');
23 | }
24 |
--------------------------------------------------------------------------------
/tests/integrations/routes/web.py:
--------------------------------------------------------------------------------
1 | from masonite.routes import Route
2 |
3 | ROUTES = [
4 | Route.get("/", "WelcomeController@show"),
5 | Route.get("/tenancy", "WelcomeController@show").middleware("multitenancy"),
6 | ]
7 |
--------------------------------------------------------------------------------
/tests/integrations/storage/.gitignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/storage/.gitignore
--------------------------------------------------------------------------------
/tests/integrations/storage/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/storage/public/favicon.ico
--------------------------------------------------------------------------------
/tests/integrations/storage/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/storage/public/logo.png
--------------------------------------------------------------------------------
/tests/integrations/storage/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
--------------------------------------------------------------------------------
/tests/integrations/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/integrations/templates/__init__.py
--------------------------------------------------------------------------------
/tests/integrations/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% block title %}Masonite 4{% endblock %}
9 |
10 |
14 | {% block head %}
15 |
16 | {% endblock %}
17 |
18 |
19 | {% block content %}{% endblock %}
20 | {% block js %}
21 |
22 | {% endblock %}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/integrations/templates/errors/403.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Permission Denied
9 |
10 |
11 |
12 |
13 | {% block content %}
14 |
15 |
Oops looks like you don't have access to this page !
16 |
17 | {% endblock %}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/integrations/templates/errors/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Page Not Found
9 |
10 |
11 |
12 |
13 | {% block content %}
14 |
15 |
Oops this page does not exist !
16 |
17 | {% endblock %}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/integrations/templates/errors/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Error 500
9 |
10 |
11 |
12 |
13 | {% block content %}
14 |
15 |
Oops an error happened !
16 |
17 | {% endblock %}
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tests/integrations/templates/maintenance.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Server Maintenance
9 |
10 |
11 |
12 |
13 | {% block content %}
14 |
15 |
Sorry, this site is currently down for maintenance.
16 |
17 | {% endblock %}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/tests/integrations/templates/welcome.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Welcome on Masonite 4{% endblock %}
3 |
4 | {% block content %}
5 |
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/tests/unit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/py-package/masonite-multitenancy/23e55ee9a8878e0940ee9dfe8532005601b380b4/tests/unit/__init__.py
--------------------------------------------------------------------------------
/tests/unit/test_package.py:
--------------------------------------------------------------------------------
1 | from masonite.tests import TestCase
2 |
3 |
4 | class Testmultitenancy(TestCase):
5 | def test_example(self):
6 | self.assertTrue(True)
7 |
--------------------------------------------------------------------------------
/wsgi.py:
--------------------------------------------------------------------------------
1 | from masonite.foundation import Application, Kernel
2 | from tests.integrations.config.providers import PROVIDERS
3 | from tests.integrations.Kernel import Kernel as ApplicationKernel
4 |
5 |
6 | """Start The Application Instance."""
7 | application = Application("tests/integrations")
8 |
9 | """Now Bind important providers needed to make the framework work."""
10 | application.register_providers(Kernel, ApplicationKernel)
11 |
12 | """Now Bind important application specific providers needed to make the application work."""
13 | application.add_providers(*PROVIDERS)
14 |
--------------------------------------------------------------------------------