Skip to content

Commit

Permalink
Merge pull request #70 from tarsil/feature/extra_object
Browse files Browse the repository at this point in the history
SaffierExtra
  • Loading branch information
tarsil authored Jul 14, 2023
2 parents 28520c8 + a9585c7 commit 70ecd15
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 22 deletions.
47 changes: 47 additions & 0 deletions docs/extras.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Extras

This section refers to the extras that Saffier offer and can be used in your application without
incurring into extra overhead to make it happen.

If you are in this section, you surely read about the [auto discovery](./migrations/discovery.md)
and how it relates with the way Saffier handles and manages migrations for you.

But, what if you simply would like to use the [shell](./shell.md) or any related command offered
by Saffier that doesn't necessarily requires migration management?

The Migrate object is the way of Saffier knowing what to do and how to manage your models but there
are cases where that doesn't happen and it is not needed, for example,
**a project using [reflect models](./reflection.md)**.

A project using reflect models, means that somehow migrations are managed externally and not by
Saffier and Saffier only needs to reflect those tables back into your code, so, do you really need
the **Migrate** object here? **Short anwser is no**.

So how can you still use those features without depending on the Migrate object? Enters
[SaffierExtra](#saffierextra).

## SaffierExtra

This is the object you want to use when **you don't need Saffier to manage the migrations for you**
and yet still being able to use Saffier tools like the [shell](./shell.md).

### How does it work

Well, its actually very similar to Migrate object in terms of setup.

Let us use [Esmerald](https://esmerald.dev) again as an example like we did for the
[tips and tricks](./tips-and-tricks.md).

```python hl_lines="12 47"
{!> ../docs_src/extras/app.py !}
```

And that is it, you can use any tool that does not relate with migrations in your application.

!!! Warning
Be aware of the use of this special class in production! It is advised not to use it there.

## Note

For now, besides the migrations and the shell, Saffier does not offer any extra tools but there are
plans to add more extras in the future and `SaffierExtra` is the way to go for that setup.
6 changes: 6 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Release Notes

## 0.15.0

### Added

- [SaffierExtra](./extras.md) class allowing the use of Saffier tools without depending on the `Migrate` object.

## 0.14.2

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions docs/shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ got stuck trying to do it and wasted a lot of time?
Well, Saffier gives you that possibility completely out of the box and ready to use with your
application models.

!!! Warning
Be aware of the use of this special class in production! It is advised not to use it there.

## Important

Before reading this section, you should get familiar with the ways Saffier handles the discovery
Expand All @@ -15,6 +18,10 @@ The following examples and explanations will be using the [auto discovery](./mig
but [--app and environment variables](./migrations/discovery.md##environment-variables) approach but the
is equally valid and works in the same way.

!!! Tip
See the [extras](./extras.md) section after getting familiar with the previous. There offers
a way of using the shell without going through the **Migrate** object.

## How does it work

Saffier ecosystem is complex internally but simpler to the user. Saffier will use the application
Expand Down
51 changes: 51 additions & 0 deletions docs_src/extras/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python
"""
Generated by 'esmerald-admin createproject'
"""
import os
import sys
from pathlib import Path

from esmerald import Esmerald, Include

import saffier
from saffier import Database, Registry, SaffierExtra

database = Database("sqlite:///db.sqlite")
registry = Registry(database)


class CustomModel(saffier.Model):
name = saffier.CharField(max_length=255)
email = saffier.EmailField(max_length=255)

class Meta:
registry = registry


def build_path():
"""
Builds the path of the project and project root.
"""
Path(__file__).resolve().parent.parent
SITE_ROOT = os.path.dirname(os.path.realpath(__file__))

if SITE_ROOT not in sys.path:
sys.path.append(SITE_ROOT)
sys.path.append(os.path.join(SITE_ROOT, "apps"))


def get_application():
"""
This is optional. The function is only used for organisation purposes.
"""

app = Esmerald(
routes=[Include(namespace="my_project.urls")],
)

SaffierExtra(app=app, registry=registry)
return app


app = get_application()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ nav:
- Discovery: "migrations/discovery.md"
- Migrations: "migrations/migrations.md"
- Tips and Tricks: "tips-and-tricks.md"
- Extras: "extras.md"
- Test Client: "test-client.md"
- Saffier People: "saffier-people.md"
- Contributing: "contributing.md"
Expand Down
4 changes: 3 additions & 1 deletion saffier/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
__version__ = "0.14.2"
__version__ = "0.15.0"

from saffier.conf import settings
from saffier.conf.global_settings import SaffierSettings

from .core.extras import SaffierExtra
from .core.registry import Registry
from .db.connection import Database
from .db.constants import CASCADE, RESTRICT, SET_NULL
Expand Down Expand Up @@ -68,6 +69,7 @@
"RESTRICT",
"ReflectModel",
"Registry",
"SaffierExtra",
"SaffierSettings",
"SET_NULL",
"TextField",
Expand Down
3 changes: 3 additions & 0 deletions saffier/core/extras/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .extra import SaffierExtra

__all__ = ["SaffierExtra"]
10 changes: 10 additions & 0 deletions saffier/core/extras/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod
from typing import Any


class BaseExtra(ABC):
@abstractmethod
def set_saffier_extension(self, app: Any) -> None:
raise NotImplementedError(
"Any class implementing the extra must implement set_saffier_extension() ."
)
42 changes: 42 additions & 0 deletions saffier/core/extras/extra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

from saffier.core.extras.base import BaseExtra
from saffier.core.terminal import Print, Terminal
from saffier.migrations.constants import SAFFIER_DB, SAFFIER_EXTRA

if TYPE_CHECKING:
from saffier.core.registry import Registry

object_setattr = object.__setattr__
terminal = Terminal()
printer = Print()


@dataclass
class Config:
app: Any
registry: "Registry"


class SaffierExtra(BaseExtra):
def __init__(self, app: Any, registry: "Registry", **kwargs: Any) -> None:
super().__init__(**kwargs)
self.app = app
self.registry = registry

self.set_saffier_extension(self.app, self.registry)

def set_saffier_extension(self, app: Any, registry: "Registry") -> None:
"""
Sets a saffier dictionary for the app object.
"""
if hasattr(app, SAFFIER_DB):
printer.write_warning(
"The application already has a Migrate related configuration with the needed information. SaffierExtra will be ignored and it can be removed."
)
return

config = Config(app=app, registry=registry)
object_setattr(app, SAFFIER_EXTRA, {})
app._saffier_extra["extra"] = config
10 changes: 3 additions & 7 deletions saffier/db/models/fields/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import typing
import warnings
from datetime import date, datetime

import sqlalchemy

import saffier
from saffier.contrib.sqlalchemy.fields import IPAddress
from saffier.core.terminal import Terminal
from saffier.core.terminal import Print
from saffier.db.constants import CASCADE, SET_NULL
from saffier.db.fields import (
URL,
Expand All @@ -26,7 +25,7 @@
if typing.TYPE_CHECKING:
from saffier import Model

terminal = Terminal()
terminal = Print()


class Field:
Expand Down Expand Up @@ -324,10 +323,7 @@ def __init__(
**kwargs: typing.Any,
):
if "null" in kwargs:
message = terminal.write_warning(
"Declaring `null` on a ManyToMany relationship has no effect."
)
warnings.warn(message, UserWarning, stacklevel=2)
terminal.write_warning("Declaring `null` on a ManyToMany relationship has no effect.")

super().__init__(null=True)
self.to = to
Expand Down
25 changes: 14 additions & 11 deletions saffier/migrations/base.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import argparse
import os
import typing
from typing import Any, Callable, Optional
from typing import TYPE_CHECKING, Any, Callable, Optional

from alembic import __version__ as __alembic_version__
from alembic import command
from alembic.config import Config as AlembicConfig

from saffier import Registry
from saffier.migrations.constants import DEFAULT_TEMPLATE_NAME
from saffier.core.extras.base import BaseExtra
from saffier.migrations.constants import DEFAULT_TEMPLATE_NAME, SAFFIER_DB
from saffier.migrations.decorators import catch_errors

alembic_version = tuple([int(v) for v in __alembic_version__.split(".")[0:3]])
if TYPE_CHECKING:
from saffier.core.registry import Registry

alembic_version = tuple(int(v) for v in __alembic_version__.split(".")[0:3])
object_setattr = object.__setattr__


class MigrateConfig:
def __init__(self, migrate: typing.Any, registry: Registry, **kwargs: Any) -> None:
def __init__(self, migrate: typing.Any, registry: "Registry", **kwargs: Any) -> None:
self.migrate = migrate
self.registry = registry
self.directory = migrate.directory
Expand All @@ -43,7 +46,7 @@ def get_template_directory(self) -> Any:
return os.path.join(package_dir, "templates")


class Migrate:
class Migrate(BaseExtra):
"""
Main migration object that should be used in any application
that requires Saffier to control the migration process.
Expand All @@ -55,17 +58,17 @@ class Migrate:
def __init__(
self,
app: typing.Any,
registry: Registry,
registry: "Registry",
compare_type: bool = True,
render_as_batch: bool = True,
**kwargs: Any,
):
assert isinstance(registry, Registry), "Registry must be an instance of saffier.Registry"
) -> None:
super().__init__(**kwargs)

self.app = app
self.configure_callbacks: typing.List[Callable] = []
self.registry = registry
self.directory = str("migrations")
self.directory = "migrations"
self.alembic_ctx_kwargs = kwargs
self.alembic_ctx_kwargs["compare_type"] = compare_type
self.alembic_ctx_kwargs["render_as_batch"] = render_as_batch
Expand All @@ -77,7 +80,7 @@ def set_saffier_extension(self, app: Any) -> None:
Sets a saffier dictionary for the app object.
"""
migrate = MigrateConfig(self, self.registry, **self.alembic_ctx_kwargs)
object_setattr(app, "_saffier_db", {})
object_setattr(app, SAFFIER_DB, {})
app._saffier_db["migrate"] = migrate

def configure(self, f: Callable) -> Any:
Expand Down
1 change: 1 addition & 0 deletions saffier/migrations/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
DISCOVERY_FILES = ["application.py", "app.py", "main.py"]
DISCOVERY_FUNCTIONS = ["get_application", "get_app"]
SAFFIER_DB = "_saffier_db"
SAFFIER_EXTRA = "_saffier_extra"
EXCLUDED_COMMANDS = ["list-templates"]
IGNORE_COMMANDS = ["list-templates"]
7 changes: 5 additions & 2 deletions saffier/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DISCOVERY_FUNCTIONS,
SAFFIER_DB,
SAFFIER_DISCOVER_APP,
SAFFIER_EXTRA,
)


Expand Down Expand Up @@ -96,7 +97,9 @@ def _find_app_in_folder(

# Iterates through the elements of the module.
for attr, value in module.__dict__.items():
if callable(value) and hasattr(value, SAFFIER_DB):
if (callable(value) and hasattr(value, SAFFIER_DB)) or (
callable(value) and hasattr(value, SAFFIER_EXTRA)
):
app_path = f"{dotted_path}:{attr}"
return Scaffold(app=value, path=app_path)

Expand All @@ -106,7 +109,7 @@ def _find_app_in_folder(
app_path = f"{dotted_path}:{func}"
fn = getattr(module, func)()

if hasattr(fn, SAFFIER_DB):
if hasattr(fn, SAFFIER_DB) or hasattr(fn, SAFFIER_EXTRA):
return Scaffold(app=fn, path=app_path)

def find_app(self, path: typing.Optional[str], cwd: Path) -> Scaffold:
Expand Down
7 changes: 6 additions & 1 deletion saffier/migrations/operations/shell/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ def shell(env: MigrationEnv, kernel: bool) -> None:
"""
Starts an interactive ipython shell with all the models
and important python libraries.
This can be used with a Migration class or with SaffierExtra object lookup.
"""
registry = env.app._saffier_db["migrate"].registry
try:
registry = env.app._saffier_db["migrate"].registry
except AttributeError:
registry = env.app._saffier_extra["extra"].registry

if (
sys.platform != "win32"
Expand Down
Loading

0 comments on commit 70ecd15

Please sign in to comment.