From e66a9abb4ef9b35002ee63b0de37f27a6958c9e1 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Wed, 12 Jun 2024 16:57:36 +0100 Subject: [PATCH] Various (#38) * Add create_indexes_for_multiple_databases * Add drop_indexes_for_multiple_databases * Release 0.9.0 --- .github/workflows/publish.yml | 2 - .github/workflows/test-suite.yml | 4 + .gitignore | 37 +- docs/{ => en/docs}/contributing.md | 0 docs/{ => en/docs}/documents.md | 89 ++++- docs/{ => en/docs}/embedded-documents.md | 2 +- docs/{ => en/docs}/exceptions.md | 0 docs/{ => en/docs}/fields.md | 0 docs/{ => en/docs}/index.md | 0 docs/{ => en/docs}/managers.md | 8 +- docs/{ => en/docs}/mongoz.md | 2 +- docs/{ => en/docs}/queries.md | 4 +- docs/en/docs/registry.md | 58 ++++ docs/{ => en/docs}/release-notes.md | 16 + docs/{ => en/docs}/settings.md | 2 +- docs/{ => en/docs}/signals.md | 22 +- docs/{ => en/docs}/sponsorship.md | 0 .../docs/statics/images}/favicon.ico | Bin .../img => en/docs/statics/images}/white.png | Bin docs/{ => en/docs}/tips-and-tricks.md | 10 +- docs/en/mkdocs.yml | 99 ++++++ .../assets/bootstrap/css/bootstrap.min.css | 0 .../assets/bootstrap/js/bootstrap.min.js | 0 .../assets/css/bs-theme-overrides.css | 0 .../overrides/assets/css/esmerald.css | 0 .../overrides/assets/img}/favicon.ico | Bin .../assets/img/illustrations/meeting.svg | 0 .../assets/img/illustrations/presentation.svg | 0 .../assets/img/illustrations/teamwork.svg | 0 .../img/illustrations/web-development.svg | 0 .../overrides/assets/img}/white.png | Bin docs/{ => en}/overrides/assets/js/esmerald.js | 0 .../overrides/assets/js/startup-modern.js | 0 docs/{ => en}/overrides/home.html | 0 docs/{ => en}/overrides/nav.html | 0 docs/language_names.yml | 183 ++++++++++ docs/missing-translation.md | 4 + docs/registry.md | 34 -- docs_src/registry/asgi_fw.py | 38 +++ docs_src/registry/custom_registry.py | 2 - docs_src/registry/document_checks.py | 22 ++ docs_src/registry/model.py | 2 - mkdocs.yml | 82 ----- mongoz/__init__.py | 2 +- mongoz/core/connection/registry.py | 7 + mongoz/core/db/documents/document.py | 215 +++++++++++- mongoz/core/db/documents/metaclasses.py | 14 +- mongoz/core/db/querysets/core/manager.py | 37 +- pyproject.toml | 30 +- scripts/clean | 3 + scripts/docs.py | 317 ++++++++++++++++++ scripts/hooks.py | 254 ++++++++++++++ scripts/install | 2 +- tests/indexes/conftest.py | 2 + tests/indexes/test_indexes_drop_indexes.py | 64 ++++ tests/indexes/test_using_different_dbs.py | 53 +++ tests/models/manager/test_sort_three.py | 108 ++++++ tests/models/manager/test_sort_two.py | 64 ++++ 58 files changed, 1678 insertions(+), 216 deletions(-) rename docs/{ => en/docs}/contributing.md (100%) rename docs/{ => en/docs}/documents.md (79%) rename docs/{ => en/docs}/embedded-documents.md (94%) rename docs/{ => en/docs}/exceptions.md (100%) rename docs/{ => en/docs}/fields.md (100%) rename docs/{ => en/docs}/index.md (100%) rename docs/{ => en/docs}/managers.md (89%) rename docs/{ => en/docs}/mongoz.md (99%) rename docs/{ => en/docs}/queries.md (99%) create mode 100644 docs/en/docs/registry.md rename docs/{ => en/docs}/release-notes.md (83%) rename docs/{ => en/docs}/settings.md (96%) rename docs/{ => en/docs}/signals.md (92%) rename docs/{ => en/docs}/sponsorship.md (100%) rename docs/{overrides/assets/img => en/docs/statics/images}/favicon.ico (100%) rename docs/{overrides/assets/img => en/docs/statics/images}/white.png (100%) rename docs/{ => en/docs}/tips-and-tricks.md (95%) create mode 100644 docs/en/mkdocs.yml rename docs/{ => en}/overrides/assets/bootstrap/css/bootstrap.min.css (100%) rename docs/{ => en}/overrides/assets/bootstrap/js/bootstrap.min.js (100%) rename docs/{ => en}/overrides/assets/css/bs-theme-overrides.css (100%) rename docs/{ => en}/overrides/assets/css/esmerald.css (100%) rename docs/{statics/images => en/overrides/assets/img}/favicon.ico (100%) rename docs/{ => en}/overrides/assets/img/illustrations/meeting.svg (100%) rename docs/{ => en}/overrides/assets/img/illustrations/presentation.svg (100%) rename docs/{ => en}/overrides/assets/img/illustrations/teamwork.svg (100%) rename docs/{ => en}/overrides/assets/img/illustrations/web-development.svg (100%) rename docs/{statics/images => en/overrides/assets/img}/white.png (100%) rename docs/{ => en}/overrides/assets/js/esmerald.js (100%) rename docs/{ => en}/overrides/assets/js/startup-modern.js (100%) rename docs/{ => en}/overrides/home.html (100%) rename docs/{ => en}/overrides/nav.html (100%) create mode 100644 docs/language_names.yml create mode 100644 docs/missing-translation.md delete mode 100644 docs/registry.md create mode 100644 docs_src/registry/asgi_fw.py create mode 100644 docs_src/registry/document_checks.py delete mode 100644 mkdocs.yml create mode 100755 scripts/docs.py create mode 100644 scripts/hooks.py create mode 100644 tests/indexes/test_indexes_drop_indexes.py create mode 100644 tests/indexes/test_using_different_dbs.py create mode 100644 tests/models/manager/test_sort_three.py create mode 100644 tests/models/manager/test_sort_two.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9110f2a..305e488 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,8 +21,6 @@ jobs: run: pip install hatch - name: "Build package" run: "hatch run build_with_check" - - name: "Build docs" - run: "hatch run docs:build" - name: "Publish to PyPI" run: "scripts/publish" env: diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 4a5ca21..347231a 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -5,8 +5,12 @@ on: push: branches: - "**" + paths-ignore: + - "docs/**" pull_request: branches: ["main"] + paths-ignore: + - "docs/**" schedule: - cron: "0 0 * * *" diff --git a/.gitignore b/.gitignore index 0f4150e..b8f737f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,31 @@ -.vscode/ +# folders +*.egg-info/ +.hypothesis/ .idea/ -*.pyc -test.db -.coverage -.pytest_cache/ .mypy_cache/ +.pytest_cache/ +.scannerwork/ +.tox/ +.venv/ +.vscode/ __pycache__/ -*.egg-info/ -htmlcov/ -venv/ -.idea/ +virtualenv/ +build/ +dist/ +node_modules/ +results/ +site/ +site_lang/ +target/ + +# files +**/*.so +**/*.sqlite +*.iml +**/*_test* +.DS_Store +.coverage +.coverage.* +.python-version +coverage.* +example.sqlite diff --git a/docs/contributing.md b/docs/en/docs/contributing.md similarity index 100% rename from docs/contributing.md rename to docs/en/docs/contributing.md diff --git a/docs/documents.md b/docs/en/docs/documents.md similarity index 79% rename from docs/documents.md rename to docs/en/docs/documents.md index 9f303fe..66b3cbb 100644 --- a/docs/documents.md +++ b/docs/en/docs/documents.md @@ -6,7 +6,7 @@ As you are probably aware, MongoDB is a NoSQL database, which means it doesn't h This also means that with documents and NoSQL, **there are no joins and foreign keys**. **Mongoz** implements those documents in a more friendly interface if you are still familiar with -ORMs or even if you use something like [Mongoz][edgy]. No reason to overcomplicate, right? +ORMs or even if you use something like [Edgy][edgy]. No reason to overcomplicate, right? ## Declaring documents @@ -19,14 +19,14 @@ an instance of `Registry` from Mongoz. There are more parameters you can use and pass into the document such as [tablename](#metaclass) and a few more but more on this in this document. -Since **Mongoz** took inspiration from the interface of [Mongoz][edgy], that also means that a [Meta](#the-meta-class) +Since **Mongoz** took inspiration from the interface of [Edgy][edgy], that also means that a [Meta](#the-meta-class) class should be declared. Although this looks very simple, in fact **Mongoz** is doing a lot of work for you behind the scenes. ```python -{!> ../docs_src/documents/declaring_models.py !} +{!> ../../../docs_src/documents/declaring_models.py !} ``` ### Embedded Documents @@ -73,7 +73,7 @@ approaches. #### In a nutshell ```python -{!> ../docs_src/documents/registry/nutshell.py !} +{!> ../../../docs_src/documents/registry/nutshell.py !} ``` As you can see, when declaring the `registry` and assigning it to `registry`, that same `registry` is @@ -84,7 +84,7 @@ then used in the `Meta` of the document. Yes, you can also use the document inheritance to help you out with your documents and avoid repetition. ```python -{!> ../docs_src/documents/registry/inheritance_no_repeat.py !} +{!> ../../../docs_src/documents/registry/inheritance_no_repeat.py !} ``` As you can see, the `User` and `Product` tables are inheriting from the `BaseDocument` where the @@ -103,7 +103,7 @@ What if your class is abstract? Can you inherit the registry anyway? Of course! That doesn't change anything with the registry. ```python -{!> ../docs_src/documents/registry/inheritance_abstract.py !} +{!> ../../../docs_src/documents/registry/inheritance_abstract.py !} ``` ### Table name @@ -114,7 +114,7 @@ if a `collection` field in the `Meta` object is not declared, it will pluralise #### Document without table name ```python -{!> ../docs_src/documents/tablename/model_no_tablename.py !} +{!> ../../../docs_src/documents/tablename/model_no_tablename.py !} ``` As mentioned in the example, because a `collection` was not declared, **Mongoz** will pluralise @@ -123,14 +123,14 @@ the python class name `User` and it will become `users` in your database. #### Document with a table name ```python -{!> ../docs_src/documents/tablename/model_with_tablename.py !} +{!> ../../../docs_src/documents/tablename/model_with_tablename.py !} ``` Here the `collection` is being explicitly declared as `users`. Although it matches with a puralisation of the python class name, this could also be something else. ```python -{!> ../docs_src/documents/tablename/model_diff_tn.py !} +{!> ../../../docs_src/documents/tablename/model_diff_tn.py !} ``` In this example, the `User` class will be represented by a `db_users` mapping into the database. @@ -159,7 +159,7 @@ In this document we already mentioned abstract documents and how to use them but examples to be even clear. ```python -{!> ../docs_src/documents/abstract/simple.py !} +{!> ../../../docs_src/documents/abstract/simple.py !} ``` This document itself does not do much alone. This simply creates a `BaseDocument` and declares the @@ -173,7 +173,7 @@ case for these to be use in the first place. Let us see a more complex example and how to use it. ```python -{!> ../docs_src/documents/abstract/common.py !} +{!> ../../../docs_src/documents/abstract/common.py !} ``` This is already quite a complex example where `User` and `Product` have both common functionality @@ -233,19 +233,19 @@ The simplest and cleanest way of declaring an index with **Mongoz**. You declare the document field. ```python hl_lines="11" -{!> ../docs_src/documents/indexes/simple.py !} +{!> ../../../docs_src/documents/indexes/simple.py !} ``` #### Index via Meta ```python hl_lines="17" -{!> ../docs_src/documents/indexes/simple2.py !} +{!> ../../../docs_src/documents/indexes/simple2.py !} ``` #### Complex indexes ```python hl_lines="9 17-20" -{!> ../docs_src/documents/indexes/complex_together.py !} +{!> ../../../docs_src/documents/indexes/complex_together.py !} ``` ### Index Operations @@ -338,4 +338,65 @@ await User.drop_index("name") If you try to drop an index with a field name not declared in the document or not declared as Index, at least, a `InvalidKeyError` is raised. +#### Document checks + +If you also want to make sure that you run the proper checks for the indexes, for example, an index was dropped +from the document and you want to make sure that is reflected, you can also do it. + +The syntax is very clear ans simple: + +```python +await MyDocument.check_indexes() +``` + +For example: + +```python +await User.check_indexes() +``` + +This can be useful if you want to make sure that for every [registry](./registry.md), all the documents have the +indexes checked before hand. + +[Read more](./registry.md#run-some-document-checks) about that in that section. + + +#### Create indexes for multiple databases + +What if you have the same document in multiple databases (multi tenancy, for example) and you would like to reflect the +indexes in all of them? Mongoz comes with that option as well. + +The database names must be passed as a list or tuple of strings. + +The syntax is as simlpe as this: + +```python +await MyDocument.create_indexes_for_multiple_databases(["db_one", "db_two", "db_three"]) +``` + +For example: + +```python +await User.create_indexes_for_multiple_databases(["db_one", "db_two", "db_three"]) +``` + +#### Drop indexes for multiple databases + +What if you have the same document in multiple databases (multi tenancy, for example) and you would like to drop the +indexes in all of them? Mongoz comes with that option as well. + +The database names must be passed as a list or tuple of strings. + +The syntax is as simlpe as this: + +```python +await MyDocument.drop_indexes_for_multiple_databases(["db_one", "db_two", "db_three"]) +``` + +For example: + +```python +await User.drop_indexes_for_multiple_databases(["db_one", "db_two", "db_three"]) +``` + [edgy]: https://edgy.tarsild.io diff --git a/docs/embedded-documents.md b/docs/en/docs/embedded-documents.md similarity index 94% rename from docs/embedded-documents.md rename to docs/en/docs/embedded-documents.md index 3db1219..c63725b 100644 --- a/docs/embedded-documents.md +++ b/docs/en/docs/embedded-documents.md @@ -8,7 +8,7 @@ To define an `EmbeddedDocument` you should inherit from `mongoz.EmbeddedDocument [fields](./fields.md) in the way you would define for any other `mongoz.Document`. ```python hl_lines="4 10 14-15 19 23 28 30-31" -{!> ../docs_src/documents/embed.py !} +{!> ../../../docs_src/documents/embed.py !} ``` As you can see, the `EmbeddedDocument` is not a standlone document itself but part of the diff --git a/docs/exceptions.md b/docs/en/docs/exceptions.md similarity index 100% rename from docs/exceptions.md rename to docs/en/docs/exceptions.md diff --git a/docs/fields.md b/docs/en/docs/fields.md similarity index 100% rename from docs/fields.md rename to docs/en/docs/fields.md diff --git a/docs/index.md b/docs/en/docs/index.md similarity index 100% rename from docs/index.md rename to docs/en/docs/index.md diff --git a/docs/managers.md b/docs/en/docs/managers.md similarity index 89% rename from docs/managers.md rename to docs/en/docs/managers.md index d5dfa31..cedad23 100644 --- a/docs/managers.md +++ b/docs/en/docs/managers.md @@ -8,7 +8,7 @@ allow you to build unique tailored queries ready to be used by your documents. Let us see an example. ```python -{!> ../docs_src/managers/simple.py !} +{!> ../../../docs_src/managers/simple.py !} ``` When querying the `User` table, the `objects` (manager) is the default and **should** be always @@ -28,13 +28,13 @@ For those familiar with Django managers, the principle is exactly the same. 😀 **The managers must be type annotated ClassVar** or an error be raised. ```python -{!> ../docs_src/managers/example.py !} +{!> ../../../docs_src/managers/example.py !} ``` Let us now create new manager and use it with our previous example. ```python -{!> ../docs_src/managers/custom.py !} +{!> ../../../docs_src/managers/custom.py !} ``` These managers can be as complex as you like with as many filters as you desire. What you need is @@ -46,7 +46,7 @@ Overriding the default manager is also possible by creating the custom manager a the `objects` manager. ```python -{!> ../docs_src/managers/override.py !} +{!> ../../../docs_src/managers/override.py !} ``` !!! Warning diff --git a/docs/mongoz.md b/docs/en/docs/mongoz.md similarity index 99% rename from docs/mongoz.md rename to docs/en/docs/mongoz.md index eb2a429..a92d4dc 100644 --- a/docs/mongoz.md +++ b/docs/en/docs/mongoz.md @@ -101,7 +101,7 @@ The following is an example how to start with Mongoz and more details and exampl Use `ipython` to run the following from the console, since it supports `await`. ```python -{!> ../docs_src/quickstart/quickstart.py !} +{!> ../../../docs_src/quickstart/quickstart.py !} ``` Now you can generate some documents and insert them into the database. diff --git a/docs/queries.md b/docs/en/docs/queries.md similarity index 99% rename from docs/queries.md rename to docs/en/docs/queries.md index 5501703..dae27b3 100644 --- a/docs/queries.md +++ b/docs/en/docs/queries.md @@ -33,7 +33,7 @@ Let us get familar with queries. Let us assume you have the following `User` document defined. ```python -{!> ../docs_src/queries/document.py !} +{!> ../../../docs_src/queries/document.py !} ``` As mentioned before, Mongoz allows to use two ways of querying. Via `manager` and via `queryset`. @@ -991,7 +991,7 @@ Querying [embedded documents](./embedded-documents.md) is also easy and here the Let us see an example. ```python hl_lines="17" -{!> ../docs_src/queries/embed.py !} +{!> ../../../docs_src/queries/embed.py !} ``` We can now create some instances of the `User`. diff --git a/docs/en/docs/registry.md b/docs/en/docs/registry.md new file mode 100644 index 0000000..97e959b --- /dev/null +++ b/docs/en/docs/registry.md @@ -0,0 +1,58 @@ +# Registry + +When using the **Mongoz**, you must use the **Registry** object to tell exactly where the +database is going to be. + +Imagine the registry as a mapping between your documents and the database where is going to be written. + +And is just that, nothing else and very simple but effective object. + +The registry is also the object that you might want to use when generating migrations using +Alembic. + +```python hl_lines="19" +{!> ../../../docs_src/registry/model.py !} +``` + +## Parameters + +* **url** - The database URL to connect to. + + ```python + from mongoz import Registry + + registry = Registry(url="mongodb://localhost:27017") + ``` + +## Custom registry + +Can you have your own custom Registry? Yes, of course! You simply need to subclass the `Registry` +class and continue from there like any other python class. + +```python +{!> ../../../docs_src/registry/custom_registry.py !} +``` + +## Run some document checks + +Sometimes you might want to make sure that all the documents have the indexes up to date beforehand. This +can be particularly useful if you already have a document and some indexes or were updated, added or removed. This +functionality runs those checks for all the documents of the given registry. + +```python +{!> ../../../docs_src/registry/document_checks.py !} +``` + +### Using within a framework + +This functionality can be useful to be also plugged if you use, for example, an ASGI Framework such as Starlette, +[Lilya](https://lilya.dev) or [Esmerald](https://esmerald.dev). + +These frameworks handle the event lifecycle for you and this is where you want to make sure these checks are run beforehand. + +Since Mongoz is from the same team as [Lilya](https://lilya.dev) and [Esmerald](https://esmerald.dev), let us see how it +would look like with Esmerald. + +```python +{!> ../../../docs_src/registry/asgi_fw.py !} +``` diff --git a/docs/release-notes.md b/docs/en/docs/release-notes.md similarity index 83% rename from docs/release-notes.md rename to docs/en/docs/release-notes.md index 64c7e4f..9fb554f 100644 --- a/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -1,5 +1,21 @@ # Release Notes +## 0.9.0 + +### Added + +- `create_indexes_for_multiple_databases` allowing to iterate for each document +the creating of the indexes in multiple databases. +- [Registry document checks](./registry.md#run-some-document-checks) allowing to check beforehand all the +index changes in a document. +- [Model check indexes](./documents.md#document-checks) to do the same checks for the indexes but for each document. +- [create_indexes_for_multiple_databases](./documents.md#create-indexes-for-multiple-databases). +- [drop_indexes_for_multiple_databases](./documents.md#drop-indexes-for-multiple-databases). + +### Changed + +- Cleaned up logic to design indexes in the `metaclass`. + ## 0.8.0 ### Changed diff --git a/docs/settings.md b/docs/en/docs/settings.md similarity index 96% rename from docs/settings.md rename to docs/en/docs/settings.md index b21d6b9..621b8d0 100644 --- a/docs/settings.md +++ b/docs/en/docs/settings.md @@ -32,7 +32,7 @@ with ease. Something like this: ```python title="myproject/configs/settings.py" -{!> ../docs_src/settings/custom_settings.py !} +{!> ../../../docs_src/settings/custom_settings.py !} ``` Super simple right? Yes and that is the intention. Mongoz does not have a lot of settings but diff --git a/docs/signals.md b/docs/en/docs/signals.md similarity index 92% rename from docs/signals.md rename to docs/en/docs/signals.md index 9260743..c5d7db3 100644 --- a/docs/signals.md +++ b/docs/en/docs/signals.md @@ -105,7 +105,7 @@ in other words, **it is what is listening to a given event**. Let us see an example. Given the following document. ```python -{!> ../docs_src/signals/receiver/document.py !} +{!> ../../../docs_src/signals/receiver/document.py !} ``` You can set a trigger to send an email to the registered user upon the creation of the record by @@ -114,7 +114,7 @@ be sent **after** the creation of the record and not before. If it was before, t be the one to use. ```python hl_lines="11-12" -{!> ../docs_src/signals/receiver/post_save.py !} +{!> ../../../docs_src/signals/receiver/post_save.py !} ``` As you can see, the `post_save` decorator is pointing the `User` document, meaning, it is "listing" @@ -140,13 +140,13 @@ What if you want to use the same receiver but for multiple models? Let us now ad document. ```python -{!> ../docs_src/signals/receiver/multiple.py !} +{!> ../../../docs_src/signals/receiver/multiple.py !} ``` The way you define the receiver for both can simply be achieved like this: ```python hl_lines="11" -{!> ../docs_src/signals/receiver/post_multiple.py !} +{!> ../../../docs_src/signals/receiver/post_multiple.py !} ``` This way you can match and do any custom logic without the need of replicating yourself too much and @@ -160,7 +160,7 @@ in one place but you might want to do something else entirely and split those in You can easily achieve this like this: ```python -{!> ../docs_src/signals/receiver/multiple_receivers.py !} +{!> ../../../docs_src/signals/receiver/multiple_receivers.py !} ``` This will make sure that every receiver will execute the given defined action. @@ -172,7 +172,7 @@ If you wish to disconnect the receiver and stop it from running for a given docu achieve this in a simple way. ```python hl_lines="20 23" -{!> ../docs_src/signals/receiver/disconnect.py !} +{!> ../../../docs_src/signals/receiver/disconnect.py !} ``` ## Custom Signals @@ -185,7 +185,7 @@ Mongoz allows the custom signals to take place per your own design. Let us continue with the same example of the `User` document. ```python -{!> ../docs_src/signals/receiver/document.py !} +{!> ../../../docs_src/signals/receiver/document.py !} ``` Now you want to have a custom signal called `on_verify` specifically tailored for your `User` needs @@ -194,7 +194,7 @@ and logic. So define it, you can simply do: ```python hl_lines="17" -{!> ../docs_src/signals/custom.py !} +{!> ../../../docs_src/signals/custom.py !} ``` Yes, this simple. You simply need to add a new signal `on_verify` to the document signals and the @@ -207,7 +207,7 @@ Yes, this simple. You simply need to add a new signal `on_verify` to the documen Now you want to create a custom functionality to be listened in your new Signal. ```python hl_lines="21 30" -{!> ../docs_src/signals/register.py !} +{!> ../../../docs_src/signals/register.py !} ``` Now not only you created the new receiver `trigger_notifications` but also connected it to the @@ -221,7 +221,7 @@ custom enough for the needs of the business logic. For simplification, the example below will be a very simple logic. ```python hl_lines="17" -{!> ../docs_src/signals/logic.py !} +{!> ../../../docs_src/signals/logic.py !} ``` As you can see, the `on_verify`, it is only triggered if the user is verified and not anywhere else. @@ -231,5 +231,5 @@ As you can see, the `on_verify`, it is only triggered if the user is verified an The process of disconnecting the signal is exactly the [same as before](#disconnecting-receivers). ```python hl_lines="10" -{!> ../docs_src/signals/disconnect.py !} +{!> ../../../docs_src/signals/disconnect.py !} ``` diff --git a/docs/sponsorship.md b/docs/en/docs/sponsorship.md similarity index 100% rename from docs/sponsorship.md rename to docs/en/docs/sponsorship.md diff --git a/docs/overrides/assets/img/favicon.ico b/docs/en/docs/statics/images/favicon.ico similarity index 100% rename from docs/overrides/assets/img/favicon.ico rename to docs/en/docs/statics/images/favicon.ico diff --git a/docs/overrides/assets/img/white.png b/docs/en/docs/statics/images/white.png similarity index 100% rename from docs/overrides/assets/img/white.png rename to docs/en/docs/statics/images/white.png diff --git a/docs/tips-and-tricks.md b/docs/en/docs/tips-and-tricks.md similarity index 95% rename from docs/tips-and-tricks.md rename to docs/en/docs/tips-and-tricks.md index cb132b3..64951a3 100644 --- a/docs/tips-and-tricks.md +++ b/docs/en/docs/tips-and-tricks.md @@ -22,7 +22,7 @@ it comes with a simple and easy way of accesing the settings anywhere in the cod Something simple like this: ```python hl_lines="18-25" -{!> ../docs_src/tips/settings.py !} +{!> ../../../docs_src/tips/settings.py !} ``` As you can see, now you have the `db_connection` in one place and easy to access from anywhere in @@ -56,7 +56,7 @@ Use the example above, let us now create a new file called `utils.py` where we w the `lru_cache` technique for our `db_connection`. ```python title="utils.py" hl_lines="6" -{!> ../docs_src/tips/lru.py !} +{!> ../../../docs_src/tips/lru.py !} ``` This will make sure that from now on you will always use the same connection and registry within @@ -107,7 +107,7 @@ This structure is generated by using the As mentioned before we will have a settings file with database connection properties assembled. ```python title="my_project/configs/settings.py" hl_lines="18-19" -{!> ../docs_src/tips/settings.py !} +{!> ../../../docs_src/tips/settings.py !} ``` ### The utils @@ -115,7 +115,7 @@ As mentioned before we will have a settings file with database connection proper Now we create the `utils.py` where we appy the [LRU](#the-lru-cache) technique. ```python title="myproject/utils.py" hl_lines="6" -{!> ../docs_src/tips/lru.py !} +{!> ../../../docs_src/tips/lru.py !} ``` ### The documents @@ -125,7 +125,7 @@ same [registry](./registry.md) ```python title="myproject/apps/accounts/documents.py" hl_lines="7 12-14" -{!> ../docs_src/tips/models.py !} +{!> ../../../docs_src/tips/models.py !} ``` Here applied the [inheritance](./documents.md#with-inheritance) to make it clean and more readable in diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml new file mode 100644 index 0000000..7bfadc5 --- /dev/null +++ b/docs/en/mkdocs.yml @@ -0,0 +1,99 @@ +site_name: Mongoz +site_description: ODM with pydantic made it simple. +site_url: https://mongoz.dymmond.com +theme: + name: material + custom_dir: ../en/overrides + language: en + palette: + - scheme: default + primary: blue grey + accent: red + media: '(prefers-color-scheme: light)' + toggle: + icon: material/lightbulb + name: Switch to dark mode + - scheme: slate + media: '(prefers-color-scheme: dark)' + primary: blue grey + accent: red + toggle: + icon: material/lightbulb-outline + name: Switch to light mode + favicon: statics/images/favicon.ico + logo: statics/images/white.png + features: + - search.suggest + - search.highlight + - content.tabs.link + - content.code.copy +repo_name: dymmond/mongoz +repo_url: https://github.com/dymmond/mongoz +edit_uri: '' +plugins: +- search +- meta-descriptions: + export_csv: false + quiet: false + enable_checks: false + min_length: 50 + max_length: 160 + trim: false +- mkdocstrings: + handlers: + python: + options: + extensions: + - griffe_typingdoc + show_root_heading: true + show_if_no_docstring: true + preload_modules: + - httpx + - a2wsgi + inherited_members: true + members_order: source + separate_signature: true + unwrap_annotated: true + filters: + - '!^_' + merge_init_into_class: true + docstring_section_style: spacy + signature_crossrefs: true + show_symbol_type_heading: true + show_symbol_type_toc: true +nav: +- Mongoz: mongoz.md +- Documents: documents.md +- Embedded Documents: embedded-documents.md +- Fields: fields.md +- Queries: queries.md +- Managers: managers.md +- Signals: signals.md +- Settings: settings.md +- Registry: registry.md +- Exceptions: exceptions.md +- Tips and Tricks: tips-and-tricks.md +- Contributing: contributing.md +- Sponsorship: sponsorship.md +- Release Notes: release-notes.md +markdown_extensions: +- attr_list +- toc: + permalink: true +- markdown.extensions.codehilite: + guess_lang: false +- mdx_include: + base_path: docs +- admonition +- codehilite +- extra +- pymdownx.superfences +- pymdownx.tabbed: + alternate_style: true +- md_in_html +extra: + alternate: + - link: / + name: en - English +hooks: +- ../../scripts/hooks.py diff --git a/docs/overrides/assets/bootstrap/css/bootstrap.min.css b/docs/en/overrides/assets/bootstrap/css/bootstrap.min.css similarity index 100% rename from docs/overrides/assets/bootstrap/css/bootstrap.min.css rename to docs/en/overrides/assets/bootstrap/css/bootstrap.min.css diff --git a/docs/overrides/assets/bootstrap/js/bootstrap.min.js b/docs/en/overrides/assets/bootstrap/js/bootstrap.min.js similarity index 100% rename from docs/overrides/assets/bootstrap/js/bootstrap.min.js rename to docs/en/overrides/assets/bootstrap/js/bootstrap.min.js diff --git a/docs/overrides/assets/css/bs-theme-overrides.css b/docs/en/overrides/assets/css/bs-theme-overrides.css similarity index 100% rename from docs/overrides/assets/css/bs-theme-overrides.css rename to docs/en/overrides/assets/css/bs-theme-overrides.css diff --git a/docs/overrides/assets/css/esmerald.css b/docs/en/overrides/assets/css/esmerald.css similarity index 100% rename from docs/overrides/assets/css/esmerald.css rename to docs/en/overrides/assets/css/esmerald.css diff --git a/docs/statics/images/favicon.ico b/docs/en/overrides/assets/img/favicon.ico similarity index 100% rename from docs/statics/images/favicon.ico rename to docs/en/overrides/assets/img/favicon.ico diff --git a/docs/overrides/assets/img/illustrations/meeting.svg b/docs/en/overrides/assets/img/illustrations/meeting.svg similarity index 100% rename from docs/overrides/assets/img/illustrations/meeting.svg rename to docs/en/overrides/assets/img/illustrations/meeting.svg diff --git a/docs/overrides/assets/img/illustrations/presentation.svg b/docs/en/overrides/assets/img/illustrations/presentation.svg similarity index 100% rename from docs/overrides/assets/img/illustrations/presentation.svg rename to docs/en/overrides/assets/img/illustrations/presentation.svg diff --git a/docs/overrides/assets/img/illustrations/teamwork.svg b/docs/en/overrides/assets/img/illustrations/teamwork.svg similarity index 100% rename from docs/overrides/assets/img/illustrations/teamwork.svg rename to docs/en/overrides/assets/img/illustrations/teamwork.svg diff --git a/docs/overrides/assets/img/illustrations/web-development.svg b/docs/en/overrides/assets/img/illustrations/web-development.svg similarity index 100% rename from docs/overrides/assets/img/illustrations/web-development.svg rename to docs/en/overrides/assets/img/illustrations/web-development.svg diff --git a/docs/statics/images/white.png b/docs/en/overrides/assets/img/white.png similarity index 100% rename from docs/statics/images/white.png rename to docs/en/overrides/assets/img/white.png diff --git a/docs/overrides/assets/js/esmerald.js b/docs/en/overrides/assets/js/esmerald.js similarity index 100% rename from docs/overrides/assets/js/esmerald.js rename to docs/en/overrides/assets/js/esmerald.js diff --git a/docs/overrides/assets/js/startup-modern.js b/docs/en/overrides/assets/js/startup-modern.js similarity index 100% rename from docs/overrides/assets/js/startup-modern.js rename to docs/en/overrides/assets/js/startup-modern.js diff --git a/docs/overrides/home.html b/docs/en/overrides/home.html similarity index 100% rename from docs/overrides/home.html rename to docs/en/overrides/home.html diff --git a/docs/overrides/nav.html b/docs/en/overrides/nav.html similarity index 100% rename from docs/overrides/nav.html rename to docs/en/overrides/nav.html diff --git a/docs/language_names.yml b/docs/language_names.yml new file mode 100644 index 0000000..c5a15dd --- /dev/null +++ b/docs/language_names.yml @@ -0,0 +1,183 @@ +aa: Afaraf +ab: аҧсуа бызшәа +ae: avesta +af: Afrikaans +ak: Akan +am: አማርኛ +an: aragonés +ar: اللغة العربية +as: অসমীয়া +av: авар мацӀ +ay: aymar aru +az: azərbaycan dili +ba: башҡорт теле +be: беларуская мова +bg: български език +bh: भोजपुरी +bi: Bislama +bm: bamanankan +bn: বাংলা +bo: བོད་ཡིག +br: brezhoneg +bs: bosanski jezik +ca: Català +ce: нохчийн мотт +ch: Chamoru +co: corsu +cr: ᓀᐦᐃᔭᐍᐏᐣ +cs: čeština +cu: ѩзыкъ словѣньскъ +cv: чӑваш чӗлхи +cy: Cymraeg +da: dansk +de: Deutsch +dv: Dhivehi +dz: རྫོང་ཁ +ee: Eʋegbe +el: Ελληνικά +en: English +eo: Esperanto +es: español +et: eesti +eu: euskara +fa: فارسی +ff: Fulfulde +fi: suomi +fj: Vakaviti +fo: føroyskt +fr: français +fy: Frysk +ga: Gaeilge +gd: Gàidhlig +gl: galego +gu: ગુજરાતી +gv: Gaelg +ha: هَوُسَ +he: עברית +hi: हिन्दी +ho: Hiri Motu +hr: Hrvatski +ht: Kreyòl ayisyen +hu: magyar +hy: Հայերեն +hz: Otjiherero +ia: Interlingua +id: Bahasa Indonesia +ie: Interlingue +ig: Asụsụ Igbo +ii: ꆈꌠ꒿ Nuosuhxop +ik: Iñupiaq +io: Ido +is: Íslenska +it: italiano +iu: ᐃᓄᒃᑎᑐᑦ +ja: 日本語 +jv: basa Jawa +ka: ქართული +kg: Kikongo +ki: Gĩkũyũ +kj: Kuanyama +kk: қазақ тілі +kl: kalaallisut +km: ខេមរភាសា +kn: ಕನ್ನಡ +ko: 한국어 +kr: Kanuri +ks: कश्मीरी +ku: Kurdî +kv: коми кыв +kw: Kernewek +ky: Кыргызча +la: latine +lb: Lëtzebuergesch +lg: Luganda +li: Limburgs +ln: Lingála +lo: ພາສາ +lt: lietuvių kalba +lu: Tshiluba +lv: latviešu valoda +mg: fiteny malagasy +mh: Kajin M̧ajeļ +mi: te reo Māori +mk: македонски јазик +ml: മലയാളം +mn: Монгол хэл +mr: मराठी +ms: Bahasa Malaysia +mt: Malti +my: ဗမာစာ +na: Ekakairũ Naoero +nb: Norsk bokmål +nd: isiNdebele +ne: नेपाली +ng: Owambo +nl: Nederlands +nn: Norsk nynorsk +'no': Norsk +nr: isiNdebele +nv: Diné bizaad +ny: chiCheŵa +oc: occitan +oj: ᐊᓂᔑᓈᐯᒧᐎᓐ +om: Afaan Oromoo +or: ଓଡ଼ିଆ +os: ирон æвзаг +pa: ਪੰਜਾਬੀ +pi: पाऴि +pl: Polski +ps: پښتو +pt: português +qu: Runa Simi +rm: rumantsch grischun +rn: Ikirundi +ro: Română +ru: русский язык +rw: Ikinyarwanda +sa: संस्कृतम् +sc: sardu +sd: सिन्धी +se: Davvisámegiella +sg: yângâ tî sängö +si: සිංහල +sk: slovenčina +sl: slovenščina +sn: chiShona +so: Soomaaliga +sq: shqip +sr: српски језик +ss: SiSwati +st: Sesotho +su: Basa Sunda +sv: svenska +sw: Kiswahili +ta: தமிழ் +te: తెలుగు +tg: тоҷикӣ +th: ไทย +ti: ትግርኛ +tk: Türkmen +tl: Wikang Tagalog +tn: Setswana +to: faka Tonga +tr: Türkçe +ts: Xitsonga +tt: татар теле +tw: Twi +ty: Reo Tahiti +ug: ئۇيغۇرچە‎ +uk: українська мова +ur: اردو +uz: Ўзбек +ve: Tshivenḓa +vi: Tiếng Việt +vo: Volapük +wa: walon +wo: Wollof +xh: isiXhosa +yi: ייִדיש +yo: Yorùbá +za: Saɯ cueŋƅ +zh: 简体中文 +zh-hant: 繁體中文 +zu: isiZulu diff --git a/docs/missing-translation.md b/docs/missing-translation.md new file mode 100644 index 0000000..2f9f659 --- /dev/null +++ b/docs/missing-translation.md @@ -0,0 +1,4 @@ +!!! warning + The current page still doesn't have a translation for this language. + + But you can help translating it: [Contributing](https://esmerald.dev/contributing/#documentation){.internal-link target=_blank}. diff --git a/docs/registry.md b/docs/registry.md deleted file mode 100644 index 109b92e..0000000 --- a/docs/registry.md +++ /dev/null @@ -1,34 +0,0 @@ -# Registry - -When using the **Mongoz**, you must use the **Registry** object to tell exactly where the -database is going to be. - -Imagine the registry as a mapping between your documents and the database where is going to be written. - -And is just that, nothing else and very simple but effective object. - -The registry is also the object that you might want to use when generating migrations using -Alembic. - -```python hl_lines="19" -{!> ../docs_src/registry/model.py !} -``` - -## Parameters - -* **url** - The database URL to connect to. - - ```python - from mongoz import Registry - - registry = Registry(url="mongodb://localhost:27017") - ``` - -## Custom registry - -Can you have your own custom Registry? Yes, of course! You simply need to subclass the `Registry` -class and continue from there like any other python class. - -```python -{!> ../docs_src/registry/custom_registry.py !} -``` diff --git a/docs_src/registry/asgi_fw.py b/docs_src/registry/asgi_fw.py new file mode 100644 index 0000000..6b7d323 --- /dev/null +++ b/docs_src/registry/asgi_fw.py @@ -0,0 +1,38 @@ +from contextlib import asynccontextmanager + +from esmerald import Esmerald + +import mongoz + +database_uri = "mongodb://localhost:27017" +registry = mongoz.Registry(database_uri) + + +class User(mongoz.Document): + """ + The User document to be created in the database as a table + If no name is provided the in Meta class, it will generate + a "users" table for you. + """ + + is_active: bool = mongoz.Boolean(default=False) + + class Meta: + registry = registry + database = "my_db" + + +# Declare the Esmerald instance +app = Esmerald(routes=[...], on_startup=[registry.document_checks]) + + +# Or using the lifespan +@asynccontextmanager +async def lifespan(app: Esmerald): + # What happens on startup + await registry.document_checks() + yield + # What happens on shutdown + + +app = Esmerald(routes=[...], lifespan=lifespan) diff --git a/docs_src/registry/custom_registry.py b/docs_src/registry/custom_registry.py index 2429dca..14fbd6d 100644 --- a/docs_src/registry/custom_registry.py +++ b/docs_src/registry/custom_registry.py @@ -1,5 +1,3 @@ -import asyncio - import mongoz database_uri = "mongodb://localhost:27017" diff --git a/docs_src/registry/document_checks.py b/docs_src/registry/document_checks.py new file mode 100644 index 0000000..4f5c1ab --- /dev/null +++ b/docs_src/registry/document_checks.py @@ -0,0 +1,22 @@ +import mongoz + +database_uri = "mongodb://localhost:27017" +registry = mongoz.Registry(database_uri) + + +class User(mongoz.Document): + """ + The User document to be created in the database as a table + If no name is provided the in Meta class, it will generate + a "users" table for you. + """ + + is_active: bool = mongoz.Boolean(default=False) + + class Meta: + registry = registry + database = "my_db" + + +# Make sure the document checks are run +await registry.document_checks() diff --git a/docs_src/registry/model.py b/docs_src/registry/model.py index 70aba22..41114ef 100644 --- a/docs_src/registry/model.py +++ b/docs_src/registry/model.py @@ -1,5 +1,3 @@ -import asyncio - import mongoz database_uri = "mongodb://localhost:27017" diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 19cda64..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,82 +0,0 @@ -site_name: Mongoz -site_description: ODM with pydantic made it simple. -site_url: https://mongoz.dymmond.com - -theme: - name: "material" - custom_dir: docs/overrides - language: en - palette: - - scheme: "default" - primary: "blue grey" - accent: "red" - media: "(prefers-color-scheme: light)" - toggle: - icon: "material/lightbulb" - name: "Switch to dark mode" - - scheme: "slate" - media: "(prefers-color-scheme: dark)" - primary: "blue grey" - accent: "red" - toggle: - icon: "material/lightbulb-outline" - name: "Switch to light mode" - favicon: statics/images/favicon.ico - logo: statics/images/white.png - features: - - search.suggest - - search.highlight - - content.tabs.link - - content.code.copy - -repo_name: dymmond/mongoz -repo_url: https://github.com/dymmond/mongoz -edit_uri: "" -plugins: - - search - - markdownextradata: - data: data - -nav: - - Mongoz: "mongoz.md" - - Documents: "documents.md" - - Embedded Documents: "embedded-documents.md" - - Fields: "fields.md" - - Queries: "queries.md" - - Managers: "managers.md" - - Signals: "signals.md" - - Settings: "settings.md" - - Registry: "registry.md" - - Exceptions: "exceptions.md" - - Tips and Tricks: "tips-and-tricks.md" - - Contributing: "contributing.md" - - Sponsorship: "sponsorship.md" - - Release Notes: "release-notes.md" -markdown_extensions: - - attr_list - - toc: - permalink: true - - markdown.extensions.codehilite: - guess_lang: false - - mdx_include: - base_path: docs - - admonition - - codehilite - - extra - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format "" - - pymdownx.emoji: - emoji_index: !!python/name:materialx.emoji.twemoji - emoji_generator: !!python/name:materialx.emoji.to_svg - - pymdownx.tabbed: - alternate_style: true - - md_in_html - -extra: - alternate: - - link: / - name: English - lang: en diff --git a/mongoz/__init__.py b/mongoz/__init__.py index 76063e9..3edac42 100644 --- a/mongoz/__init__.py +++ b/mongoz/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.8.0" +__version__ = "0.9.0" from .conf import settings from .conf.global_settings import MongozSettings diff --git a/mongoz/core/connection/registry.py b/mongoz/core/connection/registry.py index 90f7e3a..e0a30c2 100644 --- a/mongoz/core/connection/registry.py +++ b/mongoz/core/connection/registry.py @@ -57,3 +57,10 @@ def get_database(self, name: str) -> Database: async def get_databases(self) -> Sequence[Database]: databases = await self._client.list_database_names() return list(map(self.get_database, databases)) + + async def document_checks(self) -> None: + """ + Runs the document checks for all the documents in the registry. + """ + for document in self.documents.values(): + await document.check_indexes() diff --git a/mongoz/core/db/documents/document.py b/mongoz/core/db/documents/document.py index 2416ade..f65548a 100644 --- a/mongoz/core/db/documents/document.py +++ b/mongoz/core/db/documents/document.py @@ -1,4 +1,4 @@ -from typing import Any, ClassVar, List, Mapping, Type, TypeVar, Union, cast +from typing import Any, ClassVar, Dict, List, Mapping, Tuple, Type, TypeVar, Union, cast import bson import pydantic @@ -10,7 +10,7 @@ from mongoz.core.db.documents.document_row import DocumentRow from mongoz.core.db.documents.metaclasses import EmbeddedModelMetaClass from mongoz.core.db.fields.base import MongozField -from mongoz.exceptions import InvalidKeyError +from mongoz.exceptions import InvalidKeyError, MongozException from mongoz.utils.mixins import is_operation_allowed T = TypeVar("T", bound="Document") @@ -42,10 +42,13 @@ async def create( await self.signals.post_save.send(sender=self.__class__, instance=self) return self - async def update(self, **kwargs: Any) -> "Document": + async def update( + self, collection: Union[AsyncIOMotorCollection, None] = None, **kwargs: Any + ) -> "Document": """ Updates a record on an instance level. """ + collection = collection or self.meta.collection._collection # type: ignore field_definitions = { name: (annotations, ...) for name, annotations in self.__annotations__.items() @@ -66,7 +69,7 @@ async def update(self, **kwargs: Any) -> "Document": data.update(values) await self.signals.pre_update.send(sender=self.__class__, instance=self) - await self.meta.collection._collection.update_one({"_id": self.id}, {"$set": data}) # type: ignore + await collection.update_one({"_id": self.id}, {"$set": data}) await self.signals.post_update.send(sender=self.__class__, instance=self) for k, v in data.items(): @@ -89,6 +92,15 @@ async def create_many(cls: Type["Document"], models: List["Document"]) -> List[" model.id = inserted_id return models + @classmethod + def get_collection( + cls, collection: Union[AsyncIOMotorCollection, None] = None + ) -> AsyncIOMotorCollection: + """ + Get the collection object associated with the document class. + """ + return collection if collection is not None else cls.meta.collection._collection # type: ignore + @classmethod async def create_index(cls, name: str) -> str: """ @@ -104,50 +116,222 @@ async def create_index(cls, name: str) -> str: @classmethod async def create_indexes(cls) -> List[str]: - """Create indexes defined for the collection.""" - is_operation_allowed(cls) + """ + Create indexes defined for the collection or drop for existing ones. + + This method creates indexes defined for the collection associated with the document class. + It checks if the operation is allowed for the class and then creates the indexes using the + `create_indexes` method of the collection. + + Returns: + A list of strings representing the names of the created indexes. + """ + is_operation_allowed(cls) return await cls.meta.collection._collection.create_indexes(cls.meta.indexes) # type: ignore - async def delete(self) -> int: + @classmethod + async def create_indexes_for_multiple_databases( + cls, database_names: Union[List[str], Tuple[str]] + ) -> None: + """ + Create indexes for multiple databases. + + Args: + database_names (Union[List[str], Tuple[str]]): List or tuple of database names. + + Raises: + MongozException: If database_names is not a list or tuple. + + Note: + This method creates indexes for multiple databases. It iterates over the provided + database names and retrieves the corresponding database and collection objects. + Then it calls the `create_indexes` method on the collection object with the indexes + defined in the meta class of the document. + + If `autogenerate_index` is set to True in the meta class, the database name of the + document is also added to the list of database names. + + Example: + ``` + Document.create_indexes_for_multiple_databases(["db1", "db2"]) + ``` + """ + is_operation_allowed(cls) + + if not isinstance(database_names, (list, tuple)): + raise MongozException(detail="Database names must be a list or tuple") + + database_names = list(database_names) + if not cls.meta.autogenerate_index: + database_names.append(cls.meta.database.name) # type: ignore + + for database_name in database_names: + database = cls.meta.registry.get_database(database_name) # type: ignore + collection = database.get_collection(cls.meta.collection.name) # type: ignore + await collection._collection.create_indexes(cls.meta.indexes) + + @classmethod + async def drop_indexes_for_multiple_databases( + cls, database_names: Union[List[str], Tuple[str]] + ) -> None: + """ + Drops indexes for multiple databases. + + Args: + database_names (Union[List[str], Tuple[str]]): List or tuple of database names. + + Raises: + MongozException: If database_names is not a list or tuple. + + Note: + This method drops indexes for multiple databases. It iterates over the provided + database names and retrieves the corresponding database and collection objects. + Then it calls the `drop_index` method on the collection object with the indexes + defined in the meta class of the document. + + Example: + ``` + Document.create_indexes_for_multiple_databases(["db1", "db2"]) + ``` + """ + is_operation_allowed(cls) + + if not isinstance(database_names, (list, tuple)): + raise MongozException(detail="Database names must be a list or tuple") + + database_names = list(database_names) + if not cls.meta.autogenerate_index: + database_names.append(cls.meta.database.name) # type: ignore + + for database_name in database_names: + database = cls.meta.registry.get_database(database_name) # type: ignore + collection = database.get_collection(cls.meta.collection.name) # type: ignore + await cls.check_indexes(force_drop=True, collection=collection) + + @classmethod + async def list_indexes(cls) -> List[Dict[str, Any]]: + """ + List all indexes in the collection. + + This method retrieves all the indexes defined in the collection associated with the document class. + It checks if the operation is allowed for the class and then uses the `list_indexes` method of the + collection object to fetch the indexes. + + Returns: + A list of dictionaries representing the indexes in the collection. + + """ + is_operation_allowed(cls) + + collection_indexes = [] + + async for index in cls.meta.collection._collection.list_indexes(): # type: ignore + collection_indexes.append(index) + return collection_indexes + + @classmethod + async def check_indexes( + cls, + force_drop: bool = False, + collection: Union[AsyncIOMotorCollection, None] = None, + ) -> None: + """ + Check the indexes defined in the Meta object and perform any possible drop operation. + + This method checks if the indexes defined in the Meta object are present in the collection. + If an index is defined in the Meta object but not present in the collection, it performs a drop operation + to remove the index from the collection. + + Args: + cls: The class object. + + Returns: + None + """ + is_operation_allowed(cls) + + # Creates the indexes defined in the Meta object + if not force_drop: + await cls.create_indexes() + + collection = cls.get_collection(collection) + + # Get the names of indexes in the collection + collection_indexes = {index["name"] for index in await cls.list_indexes()} + + # Get the names of indexes defined in the Meta object + document_index_names = {index.name for index in cls.meta.indexes} + + # Find the indexes that are present in one set but not in the other + symmetric_difference = collection_indexes.symmetric_difference(document_index_names) + + # Remove the "_id_" index from the symmetric difference + symmetric_difference.discard("_id_") + + # Drop the indexes that are present in the collection but not in the Meta object + for name in symmetric_difference: + await collection.drop_index(name) + + # Check if the indexes defined in the Meta object are present in the collection + # And perform any possible drop operation + for name in collection_indexes: + if name in symmetric_difference: + continue + if ( + cls.model_fields.get(name, None) is not None + and not cls.model_fields.get(name).index # type: ignore + ): + await cls.drop_index(name, collection) + + async def delete(self, collection: Union[AsyncIOMotorCollection, None] = None) -> int: """Delete the document.""" is_operation_allowed(self) + collection = collection or self.meta.collection._collection # type: ignore await self.signals.pre_delete.send(sender=self.__class__, instance=self) - result = await self.meta.collection._collection.delete_one({"_id": self.id}) # type: ignore - + result = await collection.delete_one({"_id": self.id}) await self.signals.post_delete.send(sender=self.__class__, instance=self) return cast(int, result.deleted_count) @classmethod - async def drop_index(cls, name: str) -> str: + async def drop_index( + cls, name: str, collection: Union[AsyncIOMotorCollection, None] = None + ) -> str: """Drop single index from Meta indexes by name. Can raise `pymongo.errors.OperationFailure`. """ is_operation_allowed(cls) + collection = cls.get_collection(collection) for index in cls.meta.indexes: if index.name == name: - await cls.meta.collection._collection.drop_index(name) # type: ignore + await collection.drop_index(name) return name raise InvalidKeyError(f"Unable to find index: {name}") @classmethod - async def drop_indexes(cls, force: bool = False) -> Union[List[str], None]: + async def drop_indexes( + cls, force: bool = False, collection: Union[AsyncIOMotorCollection, None] = None + ) -> Union[List[str], None]: """Drop all indexes defined for the collection. With `force=True`, even indexes not defined on the collection will be removed. """ is_operation_allowed(cls) + collection = cls.get_collection(collection) if force: - return await cls.meta.collection._collection.drop_indexes() # type: ignore + await collection.drop_indexes() + return None index_names = [await cls.drop_index(index.name) for index in cls.meta.indexes] return index_names - async def save(self: "Document") -> "Document": + async def save( + self: "Document", collection: Union[AsyncIOMotorCollection, None] = None + ) -> "Document": """Save the document. This is equivalent of a single instance update. @@ -165,13 +349,14 @@ async def save(self: "Document") -> "Document": await movie.save() """ is_operation_allowed(self) + collection = collection or self.meta.collection._collection # type: ignore if not self.id: return await self.create() await self.signals.pre_save.send(sender=self.__class__, instance=self) - await self.meta.collection._collection.update_one( # type: ignore + await collection.update_one( {"_id": self.id}, {"$set": self.model_dump(exclude={"id", "_id"})} ) for k, v in self.model_dump(exclude={"id"}).items(): diff --git a/mongoz/core/db/documents/metaclasses.py b/mongoz/core/db/documents/metaclasses.py index d140666..a69abc3 100644 --- a/mongoz/core/db/documents/metaclasses.py +++ b/mongoz/core/db/documents/metaclasses.py @@ -346,12 +346,14 @@ def __search_for_fields(base: Type, attrs: Any) -> None: if not new_class.is_proxy_document: # For the indexes _index: Union[Index, None] = None - if hasattr(field, "index") and field.index and field.unique: - _index = Index(name, unique=True, sparse=field.sparse) - elif hasattr(field, "index") and field.index: - _index = Index(name, sparse=field.sparse) - elif hasattr(field, "unique") and field.unique: - _index = Index(name, unique=True) + if ( + hasattr(field, "index") + and field.index + or hasattr(field, "unique") + and field.unique + ): + index_data = {"unique": field.unique, "sparse": field.sparse} + _index = Index(name, **index_data) if _index is not None: index_names = [index.name for index in meta.indexes or []] diff --git a/mongoz/core/db/querysets/core/manager.py b/mongoz/core/db/querysets/core/manager.py index 0b67ce3..666bdd9 100644 --- a/mongoz/core/db/querysets/core/manager.py +++ b/mongoz/core/db/querysets/core/manager.py @@ -67,29 +67,30 @@ def __get__(self, instance: Any, owner: Any) -> "Manager": def using(self, database_name: str) -> "Manager": """ - **Type** Public + **Type** Public - **Arguments:** - - database_name (str): string contains the database name. + **Arguments:** + - database_name (str): string contains the database name. - **Returns:** - - Object: self instance. + **Returns:** + - Object: self instance. - **Raises:** - - None + **Raises:** + - None - This method is use to select the database: - - get the data base using the get_database method form the meta \ - class registry using the database_name that provided in \ - argument. - - store the database object as database. - - get the collection from the data base based on \ - self._collection.name - - return the self instance. + This method is use to select the database: + - get the data base using the get_database method form the meta \ + class registry using the database_name that provided in \ + argument. + - store the database object as database. + - get the collection from the data base based on \ + self._collection.name + - return the self instance. """ - database = self.model_class.meta.registry.get_database(database_name) # type: ignore - self._collection = database.get_collection(self._collection.name)._collection - return self + manager: "Manager" = self.clone() + database = manager.model_class.meta.registry.get_database(database_name) + manager._collection = database.get_collection(manager._collection.name)._collection + return manager def clone(self) -> Any: manager = self.__class__.__new__(self.__class__) diff --git a/pyproject.toml b/pyproject.toml index 1e9badb..8a643fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,20 @@ testing = [ "pytest-cov>=4.0.0,<5.0.0", "requests>=2.28.2", "ruff>=0.0.256,<1.0.0", + "ipdb", + "pdbpp", +] + +docs = [ + "griffe-typingdoc>=0.2.2,<1.0", + "mkautodoc>=0.2.0,<0.3.0", + "mkdocs>=1.1.2,<2.0.0", + "mkdocs-material>=9.4.4,<10.0.0", + "mdx-include>=1.4.2,<2.0.0", + "mkdocs-macros-plugin>=0.4.0", + "mkdocs-meta-descriptions-plugin>=2.3.0", + "mkdocstrings[python]>=0.23.0,<0.30.0", + "pyyaml>=6.0,<7.0.0", ] [tool.hatch.envs.default.scripts] @@ -90,14 +104,19 @@ clean_pyc = "find . -type f -name \"*.pyc\" -delete" clean_pyi = "find . -type f -name \"*.pyi\" -delete" clean_pycache = "find . -type d -name \"*__pycache__*\" -delete" build_with_check = "hatch build; twine check dist/*" -lint = "ruff check --fix --line-length 99 mongoz tests {args}" +lint = "ruff check --fix --line-length 99 mongoz tests {args}; hatch run test:check_types;" [tool.hatch.envs.docs] -dependencies = ["mongoz[testing]"] +features = ["testing", "docs"] [tool.hatch.envs.docs.scripts] -build = "mkdocs build" -serve = "mkdocs serve --dev-addr localhost:8000" +update_languages = "scripts/docs.py update-languages" +build = "hatch run docs:update_languages; scripts/docs.py build-all" +build_lang = "hatch run docs:update_languages; scripts/docs.py build --lang {args}" +serve = "hatch run docs:update_languages; scripts/docs.py live" +dev = "hatch run docs:update_languages; scripts/docs.py serve" +serve_lang = "hatch run docs:update_languages; scripts/docs.py live --lang {args}" +new_lang = "hatch run docs:update_languages; scripts/docs.py new-lang --lang {args}" [tool.hatch.envs.test] dependencies = [ @@ -114,10 +133,11 @@ dependencies = [ "requests>=2.28.2", "ruff>=0.0.256,<1.0.0", ] + [tool.hatch.envs.test.scripts] # needs docker services running test = "pytest {args}" -test_detail = "pytest {args} --disable-pytest-warnings -s -vv" +test_man = "pytest {args} --disable-pytest-warnings -s -vv" coverage = "pytest --cov=asyncz --cov=tests --cov-report=term-missing:skip-covered --cov-report=html tests {args}" check_types = "mypy -p mongoz" diff --git a/scripts/clean b/scripts/clean index 826773e..f64f85e 100755 --- a/scripts/clean +++ b/scripts/clean @@ -6,6 +6,9 @@ fi if [ -d 'site' ] ; then rm -r site fi +if [ -d 'site_lang' ] ; then + rm -r site_lang +fi if [ -d 'htmlcov' ] ; then rm -r htmlcov fi diff --git a/scripts/docs.py b/scripts/docs.py new file mode 100755 index 0000000..f475edf --- /dev/null +++ b/scripts/docs.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python +from __future__ import annotations + +import os +import shutil +import subprocess +from http.server import HTTPServer, SimpleHTTPRequestHandler +from multiprocessing import Pool +from pathlib import Path +from typing import Any, Dict, List + +import click +import mkdocs.commands.build +import mkdocs.commands.serve +import mkdocs.config +import mkdocs.utils +import yaml + +mkdocs_name = "mkdocs.yml" + +missing_translation_snippet = """ +{!../../../docs/missing-translation.md!} +""" + +docs_path = Path("docs") +en_docs_path = Path("docs/en") +en_config_path: Path = en_docs_path / mkdocs_name +site_path = Path("site").absolute() + +site_lang: str = "site_lang" +build_site_path = Path(site_lang).absolute() + + +@click.group() +def cli(): ... + + +def get_en_config() -> Dict[str, Any]: + """ + Get the English configuration from the specified file. + + Returns: + A dictionary containing the English configuration. + """ + return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) + + +def get_lang_paths() -> List[Path]: + """ + Returns a sorted list of paths to language files. + + Returns: + List[Path]: A sorted list of paths to language files. + """ + return sorted(docs_path.iterdir()) + + +def complete_existing_lang(incomplete: str): + """ + Generate a list of existing languages that start with the given incomplete string. + + Args: + incomplete (str): The incomplete string to match against. + + Yields: + str: The names of the existing languages that start with the given incomplete string. + """ + for lang_path in get_lang_paths(): + if lang_path.is_dir() and lang_path.name.startswith(incomplete): + yield lang_path.name + + +def get_updated_config_content() -> Dict[str, Any]: + """ + Get the updated configuration content with alternate language links. + + Returns: + Dict[str, Any]: The updated configuration content. + """ + config = get_en_config() + languages = [{"en": "/"}] + new_alternate: List[Dict[str, str]] = [] + + # Load local language names from language_names.yml + language_names_path = Path(__file__).parent / "../docs/language_names.yml" + local_language_names: Dict[str, str] = mkdocs.utils.yaml_load( + language_names_path.read_text(encoding="utf-8") + ) + + # Add alternate language links to the configuration + for lang_path in get_lang_paths(): + if lang_path.name in {"en", "em"} or not lang_path.is_dir(): + continue + code = lang_path.name + languages.append({code: f"/{code}/"}) + + for lang_dict in languages: + code = list(lang_dict.keys())[0] + url = lang_dict[code] + if code not in local_language_names: + print(f"Missing language name for: {code}, update it in docs/language_names.yml") + raise click.Abort() + use_name = f"{code} - {local_language_names[code]}" + new_alternate.append({"link": url, "name": use_name}) + + # Update the configuration with the new alternate links + config["extra"]["alternate"] = new_alternate + + return config + + +def update_config() -> None: + """ + Update the configuration file with the updated content. + + This function reads the English configuration file, generates the updated content + with alternate language links, and writes it back to the file. + + Returns: + None + """ + # Read the English configuration file + config = get_updated_config_content() + + # Write the updated content to the file + en_config_path.write_text( + yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), + encoding="utf-8", + ) + + +def build_site(lang: str = "en") -> None: + """ + Build the documentation site for a specific language. + + Args: + lang (str): The language code. Defaults to "en". + + Returns: + None + """ + lang_path = Path("docs") / lang + if not lang_path.is_dir(): + click.echo(f"Language not found: {lang}") + raise click.Abort() + + click.echo(f"Building site for: {lang}") + build_site_dist_path = build_site_path / lang + dist_path = site_path if lang == "en" else site_path / lang + + current_dir = os.getcwd() + os.chdir(lang_path) + shutil.rmtree(build_site_dist_path, ignore_errors=True) + subprocess.run(["mkdocs", "build", "--site-dir", build_site_dist_path], check=True) + shutil.copytree(build_site_dist_path, dist_path, dirs_exist_ok=True) + os.chdir(current_dir) + click.echo(f"Built site for: {lang}") + + +@cli.command() +@click.option("-l", "--lang") +def new_lang(lang: str): + """ + Generate a new docs translation directory for the language LANG. + + Args: + lang (str): The language code. + + Raises: + click.Abort: If the language directory already exists. + + Returns: + None + """ + new_path: Path = Path("docs") / lang + if new_path.exists(): + click.echo(f"The language was already created: {lang}") + raise click.Abort() + new_path.mkdir() + new_config_path: Path = Path(new_path) / mkdocs_name + new_config_path.write_text( + f"INHERIT: ../en/mkdocs.yml\nsite_dir: '../../{site_lang}/{lang}'\n", + encoding="utf-8", + ) + new_config_docs_path: Path = new_path / "docs" + new_config_docs_path.mkdir() + en_index_path: Path = en_docs_path / "docs" / "index.md" + new_index_path: Path = new_config_docs_path / "index.md" + en_index_content = en_index_path.read_text(encoding="utf-8") + new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" + new_index_path.write_text(new_index_content, encoding="utf-8") + click.echo(click.style(f"Successfully initialized: {new_path}", fg="green")) + update_languages() + + +@cli.command() +@click.option("-l", "--lang", default="en") +def build_lang(lang: str) -> None: + """ + Build the docs for a language. + """ + build_site(lang) + + +@cli.command() +def build_all() -> None: + """ + Build mkdocs site for each language, resulting in a directory structure + with each language inside the ./site/ directory. + """ + # Remove the existing site directory + shutil.rmtree(site_path, ignore_errors=True) + + # Get a list of all language paths + lang_paths = [lang.name for lang in get_lang_paths() if lang.is_dir()] + + # Get the number of available CPUs + cpu_count = os.cpu_count() or 1 + + # Set the process pool size to the number of CPUs + process_pool_size = cpu_count + click.echo(f"Using process pool size: {process_pool_size}") + + # Create a process pool + with Pool(process_pool_size) as pool: + # Build the site for each language in parallel + pool.map(build_site, lang_paths) + + +@cli.command() +def update_languages() -> None: + """ + Update the mkdocs.yml file Languages section including all the available languages. + """ + update_config() + + +@cli.command() +@click.option("-p", "--port", default=8000, help="The port to serve the documentation") +def serve(port: int) -> None: + """ + Serve a built site with translations. + + This command is used to preview a site with translations that have already been built. + It starts a simple server to serve the site on the specified port. + + Args: + port (int): The port number to serve the documentation. Defaults to 8000. + + Returns: + None + """ + click.echo("Warning: this is a very simple server.") + click.echo("For development, use the command live instead.") + click.echo("This is here only to preview a site with translations already built.") + click.echo("Make sure you run the build-all command first.") + os.chdir("site") + server_address = ("", port) + server = HTTPServer(server_address, SimpleHTTPRequestHandler) + click.echo(f"Serving at: http://127.0.0.1:{port}") + server.serve_forever() + + +@cli.command() +@click.option("-l", "--lang", default="en", help="The language code. Defaults to 'en'.") +@click.option( + "-p", "--port", default=8000, help="The port to serve the documentation. Defaults to 8000." +) +def live(lang: str, port: int) -> None: + """ + Serve a docs site with livereload for a specific language. + + This command starts a server with livereload to serve the translated files for a specific language. + It only shows the actual translated files, not the placeholders created with build-all. + + Args: + lang (str): The language code. Defaults to 'en'. + port (int): The port number to serve the documentation. Defaults to 8000. + + Returns: + None + """ + click.echo("Warning: this is a very simple server.") + lang_path: Path = docs_path / lang + os.chdir(lang_path) + mkdocs.commands.serve.serve(dev_addr=f"127.0.0.1:{port}") + + +@cli.command() +def verify_config() -> None: + """ + Verify the main mkdocs.yml content to ensure it uses the latest language names. + + This function compares the current English configuration with the updated configuration + that includes the latest language names. If they are different, it raises an error + and prompts the user to update the language names in the language_names.yml file. + + Returns: + None + """ + click.echo("Verifying mkdocs.yml") + config = get_en_config() + updated_config = get_updated_config_content() + if config != updated_config: + click.secho( + click.style( + "docs/en/mkdocs.yml is outdated from docs/language_names.yml. " + "Please update language_names.yml and run 'python ./scripts/docs.py update-languages'.", + fg="red", + ) + ) + raise click.Abort() + click.echo("Valid mkdocs.yml ✅") + + +if __name__ == "__main__": + cli() diff --git a/scripts/hooks.py b/scripts/hooks.py new file mode 100644 index 0000000..bba1b6a --- /dev/null +++ b/scripts/hooks.py @@ -0,0 +1,254 @@ +from __future__ import annotations + +from functools import lru_cache +from pathlib import Path +from typing import Any + +import material +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.files import File, Files +from mkdocs.structure.nav import Link, Navigation, Section +from mkdocs.structure.pages import Page + +non_traslated_sections = [ + "reference/", + "release-notes.md", +] + + +@lru_cache +def get_missing_translation_content(docs_dir: str) -> str: + """ + Get the missing translation content for a given docs directory. + + Args: + docs_dir (str): The path to the docs directory. + + Returns: + str: The missing translation content. + + """ + docs_dir_path = Path(docs_dir) + missing_translation_path = docs_dir_path.parent.parent / "missing-translation.md" + return missing_translation_path.read_text(encoding="utf-8") + + +@lru_cache +def get_mkdocs_material_langs() -> list[str]: + """ + Get the list of available languages in MkDocs Material theme. + + Returns: + list[str]: The list of available languages. + + """ + material_path = Path(material.__file__).parent + material_langs_path = material_path / "templates" / "partials" / "languages" + langs = [file.stem for file in material_langs_path.glob("*.html")] + return langs + + +class EnFile(File): ... + + +def on_config(config: MkDocsConfig, **kwargs: Any) -> MkDocsConfig: + """ + Modify the MkDocs configuration based on the selected language. + + Args: + config (MkDocsConfig): The MkDocs configuration object. + **kwargs (Any): Additional keyword arguments. + + Returns: + MkDocsConfig: The modified MkDocs configuration object. + + """ + available_langs = get_mkdocs_material_langs() + dir_path = Path(config.docs_dir) + lang = dir_path.parent.name + + # Set the language of the theme to the selected language + if lang in available_langs: + config.theme["language"] = lang + + # Append the selected language to the site URL if it's not already present + if not (config.site_url or "").endswith(f"{lang}/") and not lang == "en": + config.site_url = f"{config.site_url}{lang}/" + + return config + + +def resolve_file(*, item: str, files: Files, config: MkDocsConfig) -> None: + """ + Resolve a file item and add it to the list of files. + + Args: + item (str): The file item to resolve. + files (Files): The list of files. + config (MkDocsConfig): The MkDocs configuration object. + + Returns: + None + + """ + item_path = Path(config.docs_dir) / item + if not item_path.is_file(): + en_src_dir = (Path(config.docs_dir) / "../../en/docs").resolve() + potential_path = en_src_dir / item + if potential_path.is_file(): + files.append( + EnFile( + path=item, + src_dir=str(en_src_dir), + dest_dir=config.site_dir, + use_directory_urls=config.use_directory_urls, + ) + ) + + +def resolve_files(*, items: list[Any], files: Files, config: MkDocsConfig) -> None: + """ + Resolve a list of file items and add them to the list of files. + + Args: + items (list[Any]): The list of file items to resolve. + files (Files): The list of files. + config (MkDocsConfig): The MkDocs configuration object. + + Returns: + None + + """ + for item in items: + if isinstance(item, str): + resolve_file(item=item, files=files, config=config) + elif isinstance(item, dict): + assert len(item) == 1 + values = list(item.values()) + if not values: + continue + if isinstance(values[0], str): + resolve_file(item=values[0], files=files, config=config) + elif isinstance(values[0], list): + resolve_files(items=values[0], files=files, config=config) + else: + raise ValueError(f"Unexpected value: {values}") + + +def on_files(files: Files, *, config: MkDocsConfig) -> Files: + """ + Resolve the files in the MkDocs configuration and add them to the list of files. + + Args: + files (Files): The list of files. + config (MkDocsConfig): The MkDocs configuration object. + + Returns: + Files: The modified list of files. + + """ + resolve_files(items=config.nav or [], files=files, config=config) + if "logo" in config.theme: + resolve_file(item=config.theme["logo"], files=files, config=config) + if "favicon" in config.theme: + resolve_file(item=config.theme["favicon"], files=files, config=config) + resolve_files(items=config.extra_css, files=files, config=config) + resolve_files(items=config.extra_javascript, files=files, config=config) + return files + + +def generate_renamed_section_items( + items: list[Page | Section | Link], *, config: MkDocsConfig +) -> list[Page | Section | Link]: + """ + Generate renamed section items based on the MkDocs configuration. + + Args: + items (list[Page | Section | Link]): The list of section items. + config (MkDocsConfig): The MkDocs configuration object. + + Returns: + list[Page | Section | Link]: The modified list of section items. + + """ + new_items: list[Page | Section | Link] = [] + for item in items: + if isinstance(item, Section): + new_title = item.title + new_children = generate_renamed_section_items(item.children, config=config) + first_child = new_children[0] + if isinstance(first_child, Page): + if first_child.file.src_path.endswith("index.md"): + # Read the source so that the title is parsed and available + first_child.read_source(config=config) + new_title = first_child.title or new_title + # Creating a new section makes it render it collapsed by default + # no idea why, so, let's just modify the existing one + # new_section = Section(title=new_title, children=new_children) + item.title = new_title + item.children = new_children + new_items.append(item) + else: + new_items.append(item) + return new_items + + +def on_nav(nav: Navigation, *, config: MkDocsConfig, files: Files, **kwargs: Any) -> Navigation: + """ + Modify the navigation based on the MkDocs configuration. + + Args: + nav (Navigation): The navigation object. + config (MkDocsConfig): The MkDocs configuration object. + files (Files): The list of files. + **kwargs (Any): Additional keyword arguments. + + Returns: + Navigation: The modified navigation object. + + """ + new_items = generate_renamed_section_items(nav.items, config=config) + return Navigation(items=new_items, pages=nav.pages) + + +def on_pre_page(page: Page, *, config: MkDocsConfig, files: Files) -> Page: + """ + Modify the page before it is rendered. + + Args: + page (Page): The page object. + config (MkDocsConfig): The MkDocs configuration object. + files (Files): The list of files. + + Returns: + Page: The modified page object. + + """ + return page + + +def on_page_markdown(markdown: str, *, page: Page, config: MkDocsConfig, files: Files) -> str: + """ + Modify the page markdown before it is rendered. + + Args: + markdown (str): The page markdown. + page (Page): The page object. + config (MkDocsConfig): The MkDocs configuration object. + files (Files): The list of files. + + Returns: + str: The modified page markdown. + + """ + if isinstance(page.file, EnFile): + for excluded_section in non_traslated_sections: + if page.file.src_path.startswith(excluded_section): + return markdown + missing_translation_content = get_missing_translation_content(config.docs_dir) + header = "" + body = markdown + if markdown.startswith("#"): + header, _, body = markdown.partition("\n\n") + return f"{header}\n\n{missing_translation_content}\n\n{body}" + return markdown diff --git a/scripts/install b/scripts/install index baf9e80..26a4e2e 100755 --- a/scripts/install +++ b/scripts/install @@ -17,4 +17,4 @@ else fi "$PIP" install -U pip -"$PIP" install -e .[testing] +"$PIP" install -e .[testing,docs] diff --git a/tests/indexes/conftest.py b/tests/indexes/conftest.py index 369e899..0cc87ac 100644 --- a/tests/indexes/conftest.py +++ b/tests/indexes/conftest.py @@ -26,3 +26,5 @@ def event_loop() -> typing.Generator[asyncio.AbstractEventLoop, None, None]: async def test_database() -> typing.AsyncGenerator: yield await client.drop_database("test_db") + await client.drop_database("test_my_db") + await client.drop_database("test_second_db") diff --git a/tests/indexes/test_indexes_drop_indexes.py b/tests/indexes/test_indexes_drop_indexes.py new file mode 100644 index 0000000..41f4bc7 --- /dev/null +++ b/tests/indexes/test_indexes_drop_indexes.py @@ -0,0 +1,64 @@ +from typing import Optional + +import pytest + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order +from tests.conftest import client + +pytestmark = pytest.mark.anyio + +indexes = [ + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class AnotherMovie(Document): + name: str = mongoz.String() + email: str = mongoz.Email(index=True, unique=True) + year: int = mongoz.Integer() + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + indexes = indexes + database = "test_db" + autogenerate_index = True + + +async def test_drops_indexes() -> None: + await AnotherMovie.create_indexes() + await AnotherMovie.objects.create(name="Mongoz", email="mongoz@mongoz.com", year=2023) + + total_indexes = await AnotherMovie.list_indexes() + + assert len(total_indexes) == 3 + + # Change the indexes to be dropped + AnotherMovie.meta.fields["email"].index = False + AnotherMovie.meta.fields["email"].unique = False + + await AnotherMovie.check_indexes() + + total_indexes = await AnotherMovie.list_indexes() + + assert len(total_indexes) == 2 + + # Complex + await AnotherMovie.create_indexes() + + total_indexes = await AnotherMovie.list_indexes() + + assert len(total_indexes) == 3 + + # Change the indexes to be dropped + AnotherMovie.meta.fields["email"].index = False + AnotherMovie.meta.fields["email"].unique = False + + AnotherMovie.meta.indexes.pop(1) + + await AnotherMovie.check_indexes() + + total_indexes = await AnotherMovie.list_indexes() + + assert len(total_indexes) == 1 diff --git a/tests/indexes/test_using_different_dbs.py b/tests/indexes/test_using_different_dbs.py new file mode 100644 index 0000000..ac49bce --- /dev/null +++ b/tests/indexes/test_using_different_dbs.py @@ -0,0 +1,53 @@ +from typing import Optional + +import pydantic +import pytest +from pymongo.errors import DuplicateKeyError + +import mongoz +from mongoz import Document, Index, IndexType, ObjectId, Order +from tests.conftest import client + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + + +indexes = [ + Index(keys=[("year", Order.DESCENDING), ("genre", IndexType.HASHED)]), +] + + +class Movie(Document): + name: str = mongoz.String() + email: str = mongoz.Email(index=True, unique=True) + year: int = mongoz.Integer() + uuid: Optional[ObjectId] = mongoz.ObjectId(null=True) + + class Meta: + registry = client + database = "test_db" + indexes = indexes + + +async def test_model_using() -> None: + await Movie.create_indexes_for_multiple_databases(["test_my_db", "test_second_db"]) + + await Movie.objects.create(name="Mongoz", email="mongoz@mongoz.com", year=2023) + await Movie.objects.using("test_my_db").create( + name="Mongoz", email="mongoz@mongoz.com", year=2023 + ) + + await Movie.objects.using("test_second_db").create( + name="Mongoz", email="mongoz@mongoz.com", year=2023 + ) + with pytest.raises(DuplicateKeyError): + await Movie.objects.create(name="Mongoz", email="mongoz@mongoz.com", year=2023) + + with pytest.raises(DuplicateKeyError): + await Movie.objects.using("test_my_db").create( + name="Mongoz", email="mongoz@mongoz.com", year=2023 + ) + with pytest.raises(DuplicateKeyError): + await Movie.objects.using("test_second_db").create( + name="Mongoz", email="mongoz@mongoz.com", year=2023 + ) diff --git a/tests/models/manager/test_sort_three.py b/tests/models/manager/test_sort_three.py new file mode 100644 index 0000000..86e8325 --- /dev/null +++ b/tests/models/manager/test_sort_three.py @@ -0,0 +1,108 @@ +from typing import AsyncGenerator + +import pydantic +import pytest + +import mongoz +from mongoz import Document, Order +from tests.conftest import client + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + + +class Movie(Document): + idx: str = mongoz.Integer() + + class Meta: + registry = client + database = "test_db" + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.objects.delete() + yield + await Movie.drop_indexes(force=True) + await Movie.objects.delete() + + +async def test_model_sort_asc() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort(idx__asc=True).values(["idx"]) + + assert movies == [ + {"idx": 0}, + {"idx": 1}, + {"idx": 2}, + {"idx": 3}, + {"idx": 4}, + {"idx": 5}, + {"idx": 6}, + {"idx": 7}, + {"idx": 8}, + {"idx": 9}, + ] + + +async def test_model_sort_desc() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort(idx__desc=True).values(["idx"]) + + assert movies == [ + {"idx": 9}, + {"idx": 8}, + {"idx": 7}, + {"idx": 6}, + {"idx": 5}, + {"idx": 4}, + {"idx": 3}, + {"idx": 2}, + {"idx": 1}, + {"idx": 0}, + ] + + +async def test_model_sort_asc_obj() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort("idx", Order.ASCENDING).values(["idx"]) + + assert movies == [ + {"idx": 0}, + {"idx": 1}, + {"idx": 2}, + {"idx": 3}, + {"idx": 4}, + {"idx": 5}, + {"idx": 6}, + {"idx": 7}, + {"idx": 8}, + {"idx": 9}, + ] + + +async def test_model_sort_obj() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort("idx", Order.DESCENDING).values(["idx"]) + + assert movies == [ + {"idx": 9}, + {"idx": 8}, + {"idx": 7}, + {"idx": 6}, + {"idx": 5}, + {"idx": 4}, + {"idx": 3}, + {"idx": 2}, + {"idx": 1}, + {"idx": 0}, + ] diff --git a/tests/models/manager/test_sort_two.py b/tests/models/manager/test_sort_two.py new file mode 100644 index 0000000..d8d2103 --- /dev/null +++ b/tests/models/manager/test_sort_two.py @@ -0,0 +1,64 @@ +from typing import AsyncGenerator + +import pydantic +import pytest + +import mongoz +from mongoz import Document, Order +from tests.conftest import client + +pytestmark = pytest.mark.anyio +pydantic_version = pydantic.__version__[:3] + + +class Movie(Document): + idx: str = mongoz.Integer() + + class Meta: + registry = client + database = "test_db" + + +@pytest.fixture(scope="function", autouse=True) +async def prepare_database() -> AsyncGenerator: + await Movie.drop_indexes(force=True) + await Movie.objects.delete() + yield + await Movie.drop_indexes(force=True) + await Movie.objects.delete() + + +async def test_model_sort_asc() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort(idx__asc=True).values_list(["idx"], flat=True) + + assert movies == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +async def test_model_sort_desc() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort(idx__desc=True).values_list(["idx"], flat=True) + + assert movies == [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + + +async def test_model_sort_asc_obj() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort("idx", Order.ASCENDING).values_list(["idx"], flat=True) + + assert movies == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + + +async def test_model_sort_obj() -> None: + for i in range(10): + await Movie.objects.create(idx=i) + + movies = await Movie.objects.sort("idx", Order.DESCENDING).values_list(["idx"], flat=True) + + assert movies == [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]