piccolo 1.24.2__py3-none-any.whl → 1.26.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- piccolo/__init__.py +1 -1
- piccolo/apps/app/commands/new.py +47 -9
- piccolo/apps/app/commands/templates/piccolo_app.py.jinja +3 -2
- piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja +1 -1
- piccolo/apps/migrations/auto/migration_manager.py +36 -2
- piccolo/apps/schema/commands/generate.py +4 -19
- piccolo/conf/apps.py +125 -5
- piccolo/query/constraints.py +92 -0
- piccolo/query/methods/alter.py +66 -9
- piccolo/table.py +2 -0
- piccolo/utils/sync.py +7 -3
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/METADATA +1 -1
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/RECORD +21 -20
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/WHEEL +1 -1
- tests/apps/app/commands/test_new.py +40 -1
- tests/apps/migrations/auto/test_migration_manager.py +50 -0
- tests/conf/test_apps.py +49 -1
- tests/table/instance/test_remove.py +2 -0
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/entry_points.txt +0 -0
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/licenses/LICENSE +0 -0
- {piccolo-1.24.2.dist-info → piccolo-1.26.0.dist-info}/top_level.txt +0 -0
piccolo/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "1.
|
1
|
+
__VERSION__ = "1.26.0"
|
piccolo/apps/app/commands/new.py
CHANGED
@@ -2,12 +2,16 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import importlib
|
4
4
|
import os
|
5
|
+
import pathlib
|
6
|
+
import string
|
5
7
|
import sys
|
6
8
|
import typing as t
|
7
9
|
|
8
10
|
import black
|
9
11
|
import jinja2
|
10
12
|
|
13
|
+
from piccolo.conf.apps import PiccoloConfUpdater
|
14
|
+
|
11
15
|
TEMPLATE_DIRECTORY = os.path.join(
|
12
16
|
os.path.dirname(os.path.abspath(__file__)), "templates"
|
13
17
|
)
|
@@ -30,13 +34,36 @@ def module_exists(module_name: str) -> bool:
|
|
30
34
|
return True
|
31
35
|
|
32
36
|
|
33
|
-
|
34
|
-
print(f"Creating {app_name} app ...")
|
37
|
+
APP_NAME_ALLOWED_CHARACTERS = [*string.ascii_lowercase, *string.digits, "_"]
|
35
38
|
|
36
|
-
app_root = os.path.join(root, app_name)
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
+
def validate_app_name(app_name: str):
|
41
|
+
"""
|
42
|
+
Make sure the app name is something which is a valid Python package name.
|
43
|
+
|
44
|
+
:raises ValueError:
|
45
|
+
If ``app_name`` isn't valid.
|
46
|
+
|
47
|
+
"""
|
48
|
+
for char in app_name:
|
49
|
+
if not char.lower() in APP_NAME_ALLOWED_CHARACTERS:
|
50
|
+
raise ValueError(
|
51
|
+
f"The app name contains a disallowed character: `{char}`. "
|
52
|
+
"It must only include a-z, 0-9, and _ characters."
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def get_app_module(app_name: str, root: str) -> str:
|
57
|
+
return ".".join([*pathlib.Path(root).parts, app_name, "piccolo_app"])
|
58
|
+
|
59
|
+
|
60
|
+
def new_app(app_name: str, root: str = ".", register: bool = False):
|
61
|
+
print(f"Creating {app_name} app ...")
|
62
|
+
|
63
|
+
try:
|
64
|
+
validate_app_name(app_name=app_name)
|
65
|
+
except ValueError as exception:
|
66
|
+
sys.exit(str(exception))
|
40
67
|
|
41
68
|
if module_exists(app_name):
|
42
69
|
sys.exit(
|
@@ -44,7 +71,12 @@ def new_app(app_name: str, root: str = "."):
|
|
44
71
|
"Python module. Please choose a different name for your app."
|
45
72
|
)
|
46
73
|
|
47
|
-
os.
|
74
|
+
app_root = os.path.join(root, app_name)
|
75
|
+
|
76
|
+
if os.path.exists(app_root):
|
77
|
+
sys.exit("Folder already exists - exiting.")
|
78
|
+
|
79
|
+
os.makedirs(app_root)
|
48
80
|
|
49
81
|
with open(os.path.join(app_root, "__init__.py"), "w"):
|
50
82
|
pass
|
@@ -69,16 +101,22 @@ def new_app(app_name: str, root: str = "."):
|
|
69
101
|
with open(os.path.join(migrations_folder_path, "__init__.py"), "w"):
|
70
102
|
pass
|
71
103
|
|
104
|
+
if register:
|
105
|
+
app_module = get_app_module(app_name=app_name, root=root)
|
106
|
+
PiccoloConfUpdater().register_app(app_module=app_module)
|
107
|
+
|
72
108
|
|
73
|
-
def new(app_name: str, root: str = "."):
|
109
|
+
def new(app_name: str, root: str = ".", register: bool = False):
|
74
110
|
"""
|
75
111
|
Creates a new Piccolo app.
|
76
112
|
|
77
113
|
:param app_name:
|
78
114
|
The name of the new app.
|
79
115
|
:param root:
|
80
|
-
Where to create the app e.g.
|
116
|
+
Where to create the app e.g. ./my/folder. By default it creates the
|
81
117
|
app in the current directory.
|
118
|
+
:param register:
|
119
|
+
If True, the app is registered automatically in piccolo_conf.py.
|
82
120
|
|
83
121
|
"""
|
84
|
-
new_app(app_name=app_name, root=root)
|
122
|
+
new_app(app_name=app_name, root=root, register=register)
|
@@ -5,7 +5,7 @@ the APP_CONFIG.
|
|
5
5
|
|
6
6
|
import os
|
7
7
|
|
8
|
-
from piccolo.conf.apps import AppConfig, table_finder
|
8
|
+
from piccolo.conf.apps import AppConfig, table_finder, get_package
|
9
9
|
|
10
10
|
|
11
11
|
CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
|
@@ -18,7 +18,8 @@ APP_CONFIG = AppConfig(
|
|
18
18
|
'piccolo_migrations'
|
19
19
|
),
|
20
20
|
table_classes=table_finder(
|
21
|
-
modules=["
|
21
|
+
modules=[".tables"],
|
22
|
+
package=get_package(__name__),
|
22
23
|
exclude_imported=True
|
23
24
|
),
|
24
25
|
migration_dependencies=[],
|
@@ -12,7 +12,7 @@ from esmerald import (
|
|
12
12
|
post,
|
13
13
|
put,
|
14
14
|
)
|
15
|
-
from esmerald.config import StaticFilesConfig
|
15
|
+
from esmerald.core.config import StaticFilesConfig
|
16
16
|
from piccolo.engine import engine_finder
|
17
17
|
from piccolo.utils.pydantic import create_pydantic_model
|
18
18
|
from piccolo_admin.endpoints import create_admin
|
@@ -19,6 +19,7 @@ from piccolo.columns.column_types import ForeignKey, Serial
|
|
19
19
|
from piccolo.engine import engine_finder
|
20
20
|
from piccolo.query import Query
|
21
21
|
from piccolo.query.base import DDL
|
22
|
+
from piccolo.query.constraints import get_fk_constraint_name
|
22
23
|
from piccolo.schema import SchemaDDLBase
|
23
24
|
from piccolo.table import Table, create_table_class, sort_table_classes
|
24
25
|
from piccolo.utils.warnings import colored_warning
|
@@ -423,8 +424,8 @@ class MigrationManager:
|
|
423
424
|
|
424
425
|
async def _run_query(self, query: t.Union[DDL, Query, SchemaDDLBase]):
|
425
426
|
"""
|
426
|
-
If MigrationManager is
|
427
|
-
|
427
|
+
If MigrationManager is in preview mode then it just print the query
|
428
|
+
instead of executing it.
|
428
429
|
"""
|
429
430
|
if self.preview:
|
430
431
|
await self._print_query(query)
|
@@ -534,6 +535,39 @@ class MigrationManager:
|
|
534
535
|
|
535
536
|
###############################################################
|
536
537
|
|
538
|
+
on_delete = params.get("on_delete")
|
539
|
+
on_update = params.get("on_update")
|
540
|
+
if on_delete is not None or on_update is not None:
|
541
|
+
existing_table = await self.get_table_from_snapshot(
|
542
|
+
table_class_name=table_class_name,
|
543
|
+
app_name=self.app_name,
|
544
|
+
)
|
545
|
+
|
546
|
+
fk_column = existing_table._meta.get_column_by_name(
|
547
|
+
alter_column.column_name
|
548
|
+
)
|
549
|
+
|
550
|
+
assert isinstance(fk_column, ForeignKey)
|
551
|
+
|
552
|
+
# First drop the existing foreign key constraint
|
553
|
+
constraint_name = await get_fk_constraint_name(
|
554
|
+
column=fk_column
|
555
|
+
)
|
556
|
+
await self._run_query(
|
557
|
+
_Table.alter().drop_constraint(
|
558
|
+
constraint_name=constraint_name
|
559
|
+
)
|
560
|
+
)
|
561
|
+
|
562
|
+
# Then add a new foreign key constraint
|
563
|
+
await self._run_query(
|
564
|
+
_Table.alter().add_foreign_key_constraint(
|
565
|
+
column=fk_column,
|
566
|
+
on_delete=on_delete,
|
567
|
+
on_update=on_update,
|
568
|
+
)
|
569
|
+
)
|
570
|
+
|
537
571
|
null = params.get("null")
|
538
572
|
if null is not None:
|
539
573
|
await self._run_query(
|
@@ -501,6 +501,8 @@ async def get_fk_triggers(
|
|
501
501
|
Any Table subclass - just used to execute raw queries on the database.
|
502
502
|
|
503
503
|
"""
|
504
|
+
# TODO - Move this query to `piccolo.query.constraints` or use:
|
505
|
+
# `piccolo.query.constraints.referential_constraints`
|
504
506
|
triggers = await table_class.raw(
|
505
507
|
(
|
506
508
|
"SELECT tc.constraint_name, "
|
@@ -537,23 +539,6 @@ async def get_fk_triggers(
|
|
537
539
|
)
|
538
540
|
|
539
541
|
|
540
|
-
ONDELETE_MAP = {
|
541
|
-
"NO ACTION": OnDelete.no_action,
|
542
|
-
"RESTRICT": OnDelete.restrict,
|
543
|
-
"CASCADE": OnDelete.cascade,
|
544
|
-
"SET NULL": OnDelete.set_null,
|
545
|
-
"SET DEFAULT": OnDelete.set_default,
|
546
|
-
}
|
547
|
-
|
548
|
-
ONUPDATE_MAP = {
|
549
|
-
"NO ACTION": OnUpdate.no_action,
|
550
|
-
"RESTRICT": OnUpdate.restrict,
|
551
|
-
"CASCADE": OnUpdate.cascade,
|
552
|
-
"SET NULL": OnUpdate.set_null,
|
553
|
-
"SET DEFAULT": OnUpdate.set_default,
|
554
|
-
}
|
555
|
-
|
556
|
-
|
557
542
|
async def get_constraints(
|
558
543
|
table_class: t.Type[Table], tablename: str, schema_name: str = "public"
|
559
544
|
) -> TableConstraints:
|
@@ -765,8 +750,8 @@ async def create_table_class_from_db(
|
|
765
750
|
column_name, constraint_table.name
|
766
751
|
)
|
767
752
|
if trigger:
|
768
|
-
kwargs["on_update"] =
|
769
|
-
kwargs["on_delete"] =
|
753
|
+
kwargs["on_update"] = OnUpdate(trigger.on_update)
|
754
|
+
kwargs["on_delete"] = OnDelete(trigger.on_delete)
|
770
755
|
else:
|
771
756
|
output_schema.trigger_warnings.append(
|
772
757
|
f"{tablename}.{column_name}"
|
piccolo/conf/apps.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import ast
|
3
4
|
import inspect
|
4
5
|
import itertools
|
5
6
|
import os
|
@@ -11,6 +12,8 @@ from dataclasses import dataclass, field
|
|
11
12
|
from importlib import import_module
|
12
13
|
from types import ModuleType
|
13
14
|
|
15
|
+
import black
|
16
|
+
|
14
17
|
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
|
15
18
|
from piccolo.engine.base import Engine
|
16
19
|
from piccolo.table import Table
|
@@ -32,8 +35,18 @@ class PiccoloAppModule(ModuleType):
|
|
32
35
|
APP_CONFIG: AppConfig
|
33
36
|
|
34
37
|
|
38
|
+
def get_package(name: str) -> str:
|
39
|
+
"""
|
40
|
+
:param name:
|
41
|
+
The __name__ variable from a Python file.
|
42
|
+
|
43
|
+
"""
|
44
|
+
return ".".join(name.split(".")[:-1])
|
45
|
+
|
46
|
+
|
35
47
|
def table_finder(
|
36
48
|
modules: t.Sequence[str],
|
49
|
+
package: t.Optional[str] = None,
|
37
50
|
include_tags: t.Optional[t.Sequence[str]] = None,
|
38
51
|
exclude_tags: t.Optional[t.Sequence[str]] = None,
|
39
52
|
exclude_imported: bool = False,
|
@@ -46,8 +59,10 @@ def table_finder(
|
|
46
59
|
|
47
60
|
:param modules:
|
48
61
|
The module paths to check for ``Table`` subclasses. For example,
|
49
|
-
``['blog.tables']``.
|
50
|
-
|
62
|
+
``['blog.tables']``.
|
63
|
+
:param package:
|
64
|
+
This must be passed in if the modules are relative paths (e.g.
|
65
|
+
if ``modules=['.tables']`` then ``package='blog'``).
|
51
66
|
:param include_tags:
|
52
67
|
If the ``Table`` subclass has one of these tags, it will be
|
53
68
|
imported. The special tag ``'__all__'`` will import all ``Table``
|
@@ -83,10 +98,19 @@ def table_finder(
|
|
83
98
|
table_subclasses: t.List[t.Type[Table]] = []
|
84
99
|
|
85
100
|
for module_path in modules:
|
101
|
+
full_module_path = (
|
102
|
+
".".join([package, module_path.lstrip(".")])
|
103
|
+
if package
|
104
|
+
else module_path
|
105
|
+
)
|
106
|
+
|
86
107
|
try:
|
87
|
-
module = import_module(
|
108
|
+
module = import_module(
|
109
|
+
module_path,
|
110
|
+
package=package,
|
111
|
+
)
|
88
112
|
except ImportError as exception:
|
89
|
-
print(f"Unable to import {
|
113
|
+
print(f"Unable to import {full_module_path}")
|
90
114
|
raise exception from exception
|
91
115
|
|
92
116
|
object_names = [i for i in dir(module) if not i.startswith("_")]
|
@@ -100,7 +124,7 @@ def table_finder(
|
|
100
124
|
):
|
101
125
|
table: Table = _object # type: ignore
|
102
126
|
|
103
|
-
if exclude_imported and table.__module__ !=
|
127
|
+
if exclude_imported and table.__module__ != full_module_path:
|
104
128
|
continue
|
105
129
|
|
106
130
|
if exclude_tags and set(table._meta.tags).intersection(
|
@@ -416,6 +440,17 @@ class Finder:
|
|
416
440
|
else:
|
417
441
|
return module
|
418
442
|
|
443
|
+
def get_piccolo_conf_path(self) -> str:
|
444
|
+
piccolo_conf_module = self.get_piccolo_conf_module()
|
445
|
+
|
446
|
+
if piccolo_conf_module is None:
|
447
|
+
raise ModuleNotFoundError("piccolo_conf.py not found.")
|
448
|
+
|
449
|
+
module_file_path = piccolo_conf_module.__file__
|
450
|
+
assert module_file_path
|
451
|
+
|
452
|
+
return module_file_path
|
453
|
+
|
419
454
|
def get_app_registry(self) -> AppRegistry:
|
420
455
|
"""
|
421
456
|
Returns the ``AppRegistry`` instance within piccolo_conf.
|
@@ -562,3 +597,88 @@ class Finder:
|
|
562
597
|
tables.extend(app_config.table_classes)
|
563
598
|
|
564
599
|
return tables
|
600
|
+
|
601
|
+
|
602
|
+
###############################################################################
|
603
|
+
|
604
|
+
|
605
|
+
class PiccoloConfUpdater:
|
606
|
+
|
607
|
+
def __init__(self, piccolo_conf_path: t.Optional[str] = None):
|
608
|
+
"""
|
609
|
+
:param piccolo_conf_path:
|
610
|
+
The path to the piccolo_conf.py (e.g. `./piccolo_conf.py`). If not
|
611
|
+
passed in, we use our ``Finder`` class to get it.
|
612
|
+
"""
|
613
|
+
self.piccolo_conf_path = (
|
614
|
+
piccolo_conf_path or Finder().get_piccolo_conf_path()
|
615
|
+
)
|
616
|
+
|
617
|
+
def _modify_app_registry_src(self, src: str, app_module: str) -> str:
|
618
|
+
"""
|
619
|
+
:param src:
|
620
|
+
The contents of the ``piccolo_conf.py`` file.
|
621
|
+
:param app_module:
|
622
|
+
The app to add to the registry e.g. ``'music.piccolo_app'``.
|
623
|
+
:returns:
|
624
|
+
Updated Python source code string.
|
625
|
+
|
626
|
+
"""
|
627
|
+
ast_root = ast.parse(src)
|
628
|
+
|
629
|
+
parsing_successful = False
|
630
|
+
|
631
|
+
for node in ast.walk(ast_root):
|
632
|
+
if isinstance(node, ast.Call):
|
633
|
+
if (
|
634
|
+
isinstance(node.func, ast.Name)
|
635
|
+
and node.func.id == "AppRegistry"
|
636
|
+
):
|
637
|
+
if len(node.keywords) > 0:
|
638
|
+
keyword = node.keywords[0]
|
639
|
+
if keyword.arg == "apps":
|
640
|
+
apps = keyword.value
|
641
|
+
if isinstance(apps, ast.List):
|
642
|
+
apps.elts.append(
|
643
|
+
ast.Constant(app_module, kind="str")
|
644
|
+
)
|
645
|
+
parsing_successful = True
|
646
|
+
break
|
647
|
+
|
648
|
+
if not parsing_successful:
|
649
|
+
raise SyntaxError(
|
650
|
+
"Unable to parse piccolo_conf.py - `AppRegistry(apps=...)` "
|
651
|
+
"not found)."
|
652
|
+
)
|
653
|
+
|
654
|
+
new_contents = ast.unparse(ast_root)
|
655
|
+
|
656
|
+
formatted_contents = black.format_str(
|
657
|
+
new_contents, mode=black.FileMode(line_length=80)
|
658
|
+
)
|
659
|
+
|
660
|
+
return formatted_contents
|
661
|
+
|
662
|
+
def register_app(self, app_module: str):
|
663
|
+
"""
|
664
|
+
Adds the given app to the ``AppRegistry`` in ``piccolo_conf.py``.
|
665
|
+
|
666
|
+
This is used by command line tools like:
|
667
|
+
|
668
|
+
.. code-block:: bash
|
669
|
+
|
670
|
+
piccolo app new my_app --register
|
671
|
+
|
672
|
+
:param app_module:
|
673
|
+
The module of the app, e.g. ``'music.piccolo_app'``.
|
674
|
+
|
675
|
+
"""
|
676
|
+
with open(self.piccolo_conf_path) as f:
|
677
|
+
piccolo_conf_src = f.read()
|
678
|
+
|
679
|
+
new_contents = self._modify_app_registry_src(
|
680
|
+
src=piccolo_conf_src, app_module=app_module
|
681
|
+
)
|
682
|
+
|
683
|
+
with open(self.piccolo_conf_path, "wt") as f:
|
684
|
+
f.write(new_contents)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
|
3
|
+
from piccolo.columns import ForeignKey
|
4
|
+
from piccolo.columns.base import OnDelete, OnUpdate
|
5
|
+
|
6
|
+
|
7
|
+
async def get_fk_constraint_name(column: ForeignKey) -> str:
|
8
|
+
"""
|
9
|
+
Checks what the foreign key constraint is called in the database.
|
10
|
+
"""
|
11
|
+
|
12
|
+
table = column._meta.table
|
13
|
+
|
14
|
+
if table._meta.db.engine_type == "sqlite":
|
15
|
+
# TODO - add the query for SQLite
|
16
|
+
raise ValueError("SQLite isn't currently supported.")
|
17
|
+
|
18
|
+
schema = table._meta.schema or "public"
|
19
|
+
table_name = table._meta.tablename
|
20
|
+
column_name = column._meta.db_column_name
|
21
|
+
|
22
|
+
constraints = await table.raw(
|
23
|
+
"""
|
24
|
+
SELECT
|
25
|
+
kcu.constraint_name AS fk_constraint_name
|
26
|
+
FROM
|
27
|
+
information_schema.referential_constraints AS rc
|
28
|
+
INNER JOIN
|
29
|
+
information_schema.key_column_usage AS kcu
|
30
|
+
ON kcu.constraint_catalog = rc.constraint_catalog
|
31
|
+
AND kcu.constraint_schema = rc.constraint_schema
|
32
|
+
AND kcu.constraint_name = rc.constraint_name
|
33
|
+
WHERE
|
34
|
+
kcu.table_schema = {} AND
|
35
|
+
kcu.table_name = {} AND
|
36
|
+
kcu.column_name = {}
|
37
|
+
""",
|
38
|
+
schema,
|
39
|
+
table_name,
|
40
|
+
column_name,
|
41
|
+
)
|
42
|
+
|
43
|
+
return constraints[0]["fk_constraint_name"]
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class ConstraintRules:
|
48
|
+
on_delete: OnDelete
|
49
|
+
on_update: OnUpdate
|
50
|
+
|
51
|
+
|
52
|
+
async def get_fk_constraint_rules(column: ForeignKey) -> ConstraintRules:
|
53
|
+
"""
|
54
|
+
Checks the constraint rules for this foreign key in the database.
|
55
|
+
"""
|
56
|
+
table = column._meta.table
|
57
|
+
|
58
|
+
if table._meta.db.engine_type == "sqlite":
|
59
|
+
# TODO - add the query for SQLite
|
60
|
+
raise ValueError("SQLite isn't currently supported.")
|
61
|
+
|
62
|
+
schema = table._meta.schema or "public"
|
63
|
+
table_name = table._meta.tablename
|
64
|
+
column_name = column._meta.db_column_name
|
65
|
+
|
66
|
+
constraints = await table.raw(
|
67
|
+
"""
|
68
|
+
SELECT
|
69
|
+
kcu.constraint_name,
|
70
|
+
kcu.table_name,
|
71
|
+
kcu.column_name,
|
72
|
+
rc.update_rule,
|
73
|
+
rc.delete_rule
|
74
|
+
FROM
|
75
|
+
information_schema.key_column_usage AS kcu
|
76
|
+
INNER JOIN
|
77
|
+
information_schema.referential_constraints AS rc
|
78
|
+
ON kcu.constraint_name = rc.constraint_name
|
79
|
+
WHERE
|
80
|
+
kcu.table_schema = {} AND
|
81
|
+
kcu.table_name = {} AND
|
82
|
+
kcu.column_name = {}
|
83
|
+
""",
|
84
|
+
schema,
|
85
|
+
table_name,
|
86
|
+
column_name,
|
87
|
+
)
|
88
|
+
|
89
|
+
return ConstraintRules(
|
90
|
+
on_delete=OnDelete(constraints[0]["delete_rule"]),
|
91
|
+
on_update=OnUpdate(constraints[0]["update_rule"]),
|
92
|
+
)
|
piccolo/query/methods/alter.py
CHANGED
@@ -36,6 +36,18 @@ class RenameTable(AlterStatement):
|
|
36
36
|
return f"RENAME TO {self.new_name}"
|
37
37
|
|
38
38
|
|
39
|
+
@dataclass
|
40
|
+
class RenameConstraint(AlterStatement):
|
41
|
+
__slots__ = ("old_name", "new_name")
|
42
|
+
|
43
|
+
old_name: str
|
44
|
+
new_name: str
|
45
|
+
|
46
|
+
@property
|
47
|
+
def ddl(self) -> str:
|
48
|
+
return f"RENAME CONSTRAINT {self.old_name} TO {self.new_name}"
|
49
|
+
|
50
|
+
|
39
51
|
@dataclass
|
40
52
|
class AlterColumnStatement(AlterStatement):
|
41
53
|
__slots__ = ("column",)
|
@@ -194,6 +206,7 @@ class AddForeignKeyConstraint(AlterStatement):
|
|
194
206
|
"constraint_name",
|
195
207
|
"foreign_key_column_name",
|
196
208
|
"referenced_table_name",
|
209
|
+
"referenced_column_name",
|
197
210
|
"on_delete",
|
198
211
|
"on_update",
|
199
212
|
)
|
@@ -201,9 +214,9 @@ class AddForeignKeyConstraint(AlterStatement):
|
|
201
214
|
constraint_name: str
|
202
215
|
foreign_key_column_name: str
|
203
216
|
referenced_table_name: str
|
217
|
+
referenced_column_name: str
|
204
218
|
on_delete: t.Optional[OnDelete]
|
205
219
|
on_update: t.Optional[OnUpdate]
|
206
|
-
referenced_column_name: str = "id"
|
207
220
|
|
208
221
|
@property
|
209
222
|
def ddl(self) -> str:
|
@@ -273,8 +286,8 @@ class DropTable:
|
|
273
286
|
|
274
287
|
class Alter(DDL):
|
275
288
|
__slots__ = (
|
276
|
-
"_add_foreign_key_constraint",
|
277
289
|
"_add",
|
290
|
+
"_add_foreign_key_constraint",
|
278
291
|
"_drop_constraint",
|
279
292
|
"_drop_default",
|
280
293
|
"_drop_table",
|
@@ -288,6 +301,7 @@ class Alter(DDL):
|
|
288
301
|
"_set_null",
|
289
302
|
"_set_schema",
|
290
303
|
"_set_unique",
|
304
|
+
"_rename_constraint",
|
291
305
|
)
|
292
306
|
|
293
307
|
def __init__(self, table: t.Type[Table], **kwargs):
|
@@ -307,6 +321,7 @@ class Alter(DDL):
|
|
307
321
|
self._set_null: t.List[SetNull] = []
|
308
322
|
self._set_schema: t.List[SetSchema] = []
|
309
323
|
self._set_unique: t.List[SetUnique] = []
|
324
|
+
self._rename_constraint: t.List[RenameConstraint] = []
|
310
325
|
|
311
326
|
def add_column(self: Self, name: str, column: Column) -> Self:
|
312
327
|
"""
|
@@ -372,6 +387,24 @@ class Alter(DDL):
|
|
372
387
|
self._rename_table = [RenameTable(new_name=new_name)]
|
373
388
|
return self
|
374
389
|
|
390
|
+
def rename_constraint(self, old_name: str, new_name: str) -> Alter:
|
391
|
+
"""
|
392
|
+
Rename a constraint on the table::
|
393
|
+
|
394
|
+
>>> await Band.alter().rename_constraint(
|
395
|
+
... 'old_constraint_name',
|
396
|
+
... 'new_constraint_name',
|
397
|
+
... )
|
398
|
+
|
399
|
+
"""
|
400
|
+
self._rename_constraint = [
|
401
|
+
RenameConstraint(
|
402
|
+
old_name=old_name,
|
403
|
+
new_name=new_name,
|
404
|
+
)
|
405
|
+
]
|
406
|
+
return self
|
407
|
+
|
375
408
|
def rename_column(
|
376
409
|
self, column: t.Union[str, Column], new_name: str
|
377
410
|
) -> Alter:
|
@@ -488,7 +521,7 @@ class Alter(DDL):
|
|
488
521
|
def _get_constraint_name(self, column: t.Union[str, ForeignKey]) -> str:
|
489
522
|
column_name = AlterColumnStatement(column=column).column_name
|
490
523
|
tablename = self.table._meta.tablename
|
491
|
-
return f"{tablename}_{column_name}
|
524
|
+
return f"{tablename}_{column_name}_fkey"
|
492
525
|
|
493
526
|
def drop_constraint(self, constraint_name: str) -> Alter:
|
494
527
|
self._drop_constraint.append(
|
@@ -500,37 +533,58 @@ class Alter(DDL):
|
|
500
533
|
self, column: t.Union[str, ForeignKey]
|
501
534
|
) -> Alter:
|
502
535
|
constraint_name = self._get_constraint_name(column=column)
|
503
|
-
|
536
|
+
self._drop_constraint.append(
|
537
|
+
DropConstraint(constraint_name=constraint_name)
|
538
|
+
)
|
539
|
+
return self
|
504
540
|
|
505
541
|
def add_foreign_key_constraint(
|
506
542
|
self,
|
507
543
|
column: t.Union[str, ForeignKey],
|
508
|
-
referenced_table_name: str,
|
544
|
+
referenced_table_name: t.Optional[str] = None,
|
545
|
+
referenced_column_name: t.Optional[str] = None,
|
546
|
+
constraint_name: t.Optional[str] = None,
|
509
547
|
on_delete: t.Optional[OnDelete] = None,
|
510
548
|
on_update: t.Optional[OnUpdate] = None,
|
511
|
-
referenced_column_name: str = "id",
|
512
549
|
) -> Alter:
|
513
550
|
"""
|
514
551
|
Add a new foreign key constraint::
|
515
552
|
|
516
553
|
>>> await Band.alter().add_foreign_key_constraint(
|
517
554
|
... Band.manager,
|
518
|
-
... referenced_table_name='manager',
|
519
555
|
... on_delete=OnDelete.cascade
|
520
556
|
... )
|
521
557
|
|
522
558
|
"""
|
523
|
-
constraint_name = self._get_constraint_name(
|
559
|
+
constraint_name = constraint_name or self._get_constraint_name(
|
560
|
+
column=column
|
561
|
+
)
|
524
562
|
column_name = AlterColumnStatement(column=column).column_name
|
525
563
|
|
564
|
+
if referenced_column_name is None:
|
565
|
+
if isinstance(column, ForeignKey):
|
566
|
+
referenced_column_name = (
|
567
|
+
column._foreign_key_meta.resolved_target_column._meta.db_column_name # noqa: E501
|
568
|
+
)
|
569
|
+
else:
|
570
|
+
raise ValueError("Please pass in `referenced_column_name`.")
|
571
|
+
|
572
|
+
if referenced_table_name is None:
|
573
|
+
if isinstance(column, ForeignKey):
|
574
|
+
referenced_table_name = (
|
575
|
+
column._foreign_key_meta.resolved_references._meta.tablename # noqa: E501
|
576
|
+
)
|
577
|
+
else:
|
578
|
+
raise ValueError("Please pass in `referenced_table_name`.")
|
579
|
+
|
526
580
|
self._add_foreign_key_constraint.append(
|
527
581
|
AddForeignKeyConstraint(
|
528
582
|
constraint_name=constraint_name,
|
529
583
|
foreign_key_column_name=column_name,
|
530
584
|
referenced_table_name=referenced_table_name,
|
585
|
+
referenced_column_name=referenced_column_name,
|
531
586
|
on_delete=on_delete,
|
532
587
|
on_update=on_update,
|
533
|
-
referenced_column_name=referenced_column_name,
|
534
588
|
)
|
535
589
|
)
|
536
590
|
return self
|
@@ -579,9 +633,12 @@ class Alter(DDL):
|
|
579
633
|
i.ddl
|
580
634
|
for i in itertools.chain(
|
581
635
|
self._add,
|
636
|
+
self._add_foreign_key_constraint,
|
582
637
|
self._rename_columns,
|
583
638
|
self._rename_table,
|
639
|
+
self._rename_constraint,
|
584
640
|
self._drop,
|
641
|
+
self._drop_constraint,
|
585
642
|
self._drop_default,
|
586
643
|
self._set_column_type,
|
587
644
|
self._set_unique,
|
piccolo/table.py
CHANGED
piccolo/utils/sync.py
CHANGED
@@ -2,10 +2,14 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import typing as t
|
5
|
-
from concurrent.futures import ThreadPoolExecutor
|
5
|
+
from concurrent.futures import Future, ThreadPoolExecutor
|
6
6
|
|
7
|
+
ReturnType = t.TypeVar("ReturnType")
|
7
8
|
|
8
|
-
|
9
|
+
|
10
|
+
def run_sync(
|
11
|
+
coroutine: t.Coroutine[t.Any, t.Any, ReturnType],
|
12
|
+
) -> ReturnType:
|
9
13
|
"""
|
10
14
|
Run the coroutine synchronously - trying to accommodate as many edge cases
|
11
15
|
as possible.
|
@@ -20,5 +24,5 @@ def run_sync(coroutine: t.Coroutine):
|
|
20
24
|
except RuntimeError:
|
21
25
|
# An event loop already exists.
|
22
26
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
23
|
-
future = executor.submit(asyncio.run, coroutine)
|
27
|
+
future: Future = executor.submit(asyncio.run, coroutine)
|
24
28
|
return future.result()
|
@@ -1,18 +1,18 @@
|
|
1
|
-
piccolo/__init__.py,sha256=
|
1
|
+
piccolo/__init__.py,sha256=ZVRfjDuz3xhQA4e78KxBcezkGjqcWBkBGlP0bm-NFD8,23
|
2
2
|
piccolo/custom_types.py,sha256=7HMQAze-5mieNLfbQ5QgbRQgR2abR7ol0qehv2SqROY,604
|
3
3
|
piccolo/main.py,sha256=1VsFV67FWTUikPTysp64Fmgd9QBVa_9wcwKfwj2UCEA,5117
|
4
4
|
piccolo/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
piccolo/querystring.py,sha256=kb7RYTvQZEyPsC4GH8vR2b_w35wnM-ita242S0_eyvQ,10013
|
6
6
|
piccolo/schema.py,sha256=qNNy4tG_HqnXR9t3hHMgYXtGxHabwQAhUpc6RKLJ_gE,7960
|
7
|
-
piccolo/table.py,sha256=
|
7
|
+
piccolo/table.py,sha256=vA4H5oebvpIsUYUpAIOFMqAG8wtNBWmu18zEmbfen0I,50629
|
8
8
|
piccolo/table_reflection.py,sha256=02baOSLX6f2LEo0kruFZYF_nPPTbIvaCTH_KPGe0DKw,7540
|
9
9
|
piccolo/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
10
|
piccolo/apps/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
11
|
piccolo/apps/app/piccolo_app.py,sha256=8z2ITpxQQ-McxSYwQ5H_vyEnRXbY6cyAh2JSqhiylYk,340
|
12
12
|
piccolo/apps/app/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
piccolo/apps/app/commands/new.py,sha256=
|
13
|
+
piccolo/apps/app/commands/new.py,sha256=XzcVBBHiEezIt0VvyNezLDSgcSRDn5rmefdVulReCSk,3382
|
14
14
|
piccolo/apps/app/commands/show_all.py,sha256=46Hv3SipMT0YeMgAobU62O0mR7xvN1pn7xjU9Y2spKM,252
|
15
|
-
piccolo/apps/app/commands/templates/piccolo_app.py.jinja,sha256=
|
15
|
+
piccolo/apps/app/commands/templates/piccolo_app.py.jinja,sha256=QjaBEQucryT-vCv37Qm47I2hdirGxYE725WUGqF2WR0,592
|
16
16
|
piccolo/apps/app/commands/templates/tables.py.jinja,sha256=revzdrvDDwe78VedBKz0zYSwcsxyv2IURun6q6qmV1Y,32
|
17
17
|
piccolo/apps/asgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
18
|
piccolo/apps/asgi/piccolo_app.py,sha256=7VUvqQJbB-ScO0A62S6MiJmQL9F5DS-SdlqlDLbAblE,217
|
@@ -20,7 +20,7 @@ piccolo/apps/asgi/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
|
|
20
20
|
piccolo/apps/asgi/commands/new.py,sha256=718mXx7XdDTN0CKK0ZB1WVMkOrQtVfqT5bqO1kDKnRk,4335
|
21
21
|
piccolo/apps/asgi/commands/templates/app/README.md.jinja,sha256=As3gNEZt9qcRmTVkjCzNtXJ8r4-3g0fCSe7Q-P39ezI,214
|
22
22
|
piccolo/apps/asgi/commands/templates/app/_blacksheep_app.py.jinja,sha256=IKOql1G5wrEKm5qErlizOmrwYKlnxkm-d8NY5uVg9KA,3186
|
23
|
-
piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja,sha256=
|
23
|
+
piccolo/apps/asgi/commands/templates/app/_esmerald_app.py.jinja,sha256=fIbD106Us4j2DBv_41PimvFRVw2FuZhOfc3xFPkNjuY,2715
|
24
24
|
piccolo/apps/asgi/commands/templates/app/_falcon_app.py.jinja,sha256=LOn3auJFeXNW48rtHzRbH3MzxWbRNhFib6Fm6wDS53E,1684
|
25
25
|
piccolo/apps/asgi/commands/templates/app/_fastapi_app.py.jinja,sha256=mKnYfUOnYyWJA1jFoRLCUOGQlK6imaxx_1qaauGjeeQ,2627
|
26
26
|
piccolo/apps/asgi/commands/templates/app/_lilya_app.py.jinja,sha256=PUph5Jj_AXVpxXZmpUzzHXogUchU8vjKBL_7WvgrfCU,1260
|
@@ -66,7 +66,7 @@ piccolo/apps/migrations/piccolo_app.py,sha256=1EcS2ComBPCaMCC2C3WaPR_GqLwt3XiIJN
|
|
66
66
|
piccolo/apps/migrations/tables.py,sha256=jqBnK-Rk545v1Eu6GaLHTVz7-uwBTUnz2m58OA-mxTc,799
|
67
67
|
piccolo/apps/migrations/auto/__init__.py,sha256=eYb1rZQaalumv_bhbcEe6x3dUglmpFtw7Egg6k7597U,316
|
68
68
|
piccolo/apps/migrations/auto/diffable_table.py,sha256=1HdqGeWFUYVJ2cJg6DZWOCh67SbgCxFVc554uD7N71A,7405
|
69
|
-
piccolo/apps/migrations/auto/migration_manager.py,sha256=
|
69
|
+
piccolo/apps/migrations/auto/migration_manager.py,sha256=5M2_01_FsZj6zIGXLEwkyEAPF2e-ICNB2_ZcBWwifCQ,37267
|
70
70
|
piccolo/apps/migrations/auto/operations.py,sha256=169IrCLR3FtTRxHsEHNg6dTG45lcEM7Aoyy3SwgX_hU,1329
|
71
71
|
piccolo/apps/migrations/auto/schema_differ.py,sha256=VA1rK-_wNSdyZZgfA3ZOlpVGJCcvLyouKtT9k2YKhiA,26266
|
72
72
|
piccolo/apps/migrations/auto/schema_snapshot.py,sha256=ZqUg4NpChOeoACKF2gkhqsz1BW3wOWFnzJCccq-CNNQ,4719
|
@@ -93,7 +93,7 @@ piccolo/apps/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
93
93
|
piccolo/apps/schema/piccolo_app.py,sha256=De9eujzB6zWsP6J1gHYUk_f5_DpjvTZVXJsQ3eXBgnA,432
|
94
94
|
piccolo/apps/schema/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
95
95
|
piccolo/apps/schema/commands/exceptions.py,sha256=ZOGL3iV-xtWbWsImXObrXNaKtNPY_Qk1OmaOMOV6Ps0,236
|
96
|
-
piccolo/apps/schema/commands/generate.py,sha256=
|
96
|
+
piccolo/apps/schema/commands/generate.py,sha256=xYRz9P4cdahSHzAIiXhXBzg6eUqV0WB2THSRZH7uPhU,30415
|
97
97
|
piccolo/apps/schema/commands/graph.py,sha256=FuQUPavUXpr-Y_11XRr11DbVLsgK8uG0IN8uBZIe5G4,3190
|
98
98
|
piccolo/apps/schema/commands/templates/graphviz.dot.jinja,sha256=-legygtsueOC70aboX35ZJpbCAXcv3E8RXXvFDQTeIY,1443
|
99
99
|
piccolo/apps/shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -143,7 +143,7 @@ piccolo/columns/operators/comparison.py,sha256=G7bI_O-EXqun_zHwbNcZ9z9gsY8OK-0oB
|
|
143
143
|
piccolo/columns/operators/math.py,sha256=knsUZzYOVdsFn3bTS0XC0ZzfNObeJcMvZ8Q_QwmGxjU,325
|
144
144
|
piccolo/columns/operators/string.py,sha256=M5ifxHP-ttJaE_wYCl23W5sJof4i5S5_QDIOv34VxDM,142
|
145
145
|
piccolo/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
146
|
-
piccolo/conf/apps.py,sha256=
|
146
|
+
piccolo/conf/apps.py,sha256=t5_Wn9A8_NRgSKmkkXqnq36yi-qucnpll41_XeX9nYE,22590
|
147
147
|
piccolo/engine/__init__.py,sha256=Z0QR5NAA9jTFenY7pEJv1C8jZXBaFZojBUR3z3nx1cw,283
|
148
148
|
piccolo/engine/base.py,sha256=4NISWRuvgp5ShqJbOEK6g8ok8Ijqtp2gzuM9J6o_wSU,6444
|
149
149
|
piccolo/engine/cockroach.py,sha256=gGnihplotMZMWqHwRnZYnnbKU3jFrwttwOlNtktoeLE,1522
|
@@ -153,6 +153,7 @@ piccolo/engine/postgres.py,sha256=DekL3KafCdzSAEQ6_EgOiUB1ERXh2xpePYwI9QvmN-c,18
|
|
153
153
|
piccolo/engine/sqlite.py,sha256=Oe0GBrIUSUkutvk5LoXGWC6HFQzKeusfql5-NMssH_s,25735
|
154
154
|
piccolo/query/__init__.py,sha256=bcsMV4813rMRAIqGv4DxI4eyO4FmpXkDv9dfTk5pt3A,699
|
155
155
|
piccolo/query/base.py,sha256=sO5VyicbWjgYaQukr6jqUqUUrOctL6QJ1MjcsgDKHXM,14912
|
156
|
+
piccolo/query/constraints.py,sha256=menFcqLKSM4697OSvMRZPsTgxHfR9GTvZZMrGDk2PrA,2601
|
156
157
|
piccolo/query/mixins.py,sha256=X9HEYnj6uOjgTkGr4vgqTwN_dokJPzVagwbFx385atQ,24468
|
157
158
|
piccolo/query/proxy.py,sha256=Yq4jNc7IWJvdeO3u7_7iPyRy2WhVj8KsIUcIYHBIi9Q,1839
|
158
159
|
piccolo/query/functions/__init__.py,sha256=pZkzOIh7Sg9HPNOeegOwAS46Oxt31ATlSVmwn-lxCbc,605
|
@@ -163,7 +164,7 @@ piccolo/query/functions/math.py,sha256=2Wapq0lpXZh77z0uzXUhnOfmUkbkM0xjQ4tiyuCsb
|
|
163
164
|
piccolo/query/functions/string.py,sha256=X3g_4qomJJCkYOcKcK-zZEqC6qJBrS4VTogPp9Xw4Cs,2506
|
164
165
|
piccolo/query/functions/type_conversion.py,sha256=OYbZc6TEk6b5yTwCMw2rmZ-UiQiUiWZOyxwMLzUjXwE,2583
|
165
166
|
piccolo/query/methods/__init__.py,sha256=tm4gLeV_obDqpgnouVjFbGubbaoJcqm_cbNd4LPo48Q,622
|
166
|
-
piccolo/query/methods/alter.py,sha256=
|
167
|
+
piccolo/query/methods/alter.py,sha256=tL8IIuEldBwegQOZM5N4IaoO2fH0tYLk8R7ra6PLD3s,18519
|
167
168
|
piccolo/query/methods/count.py,sha256=Vxn_7Ry-rleC6OGRxh-cLbuEMsy1DNjAZJThGED-_do,1748
|
168
169
|
piccolo/query/methods/create.py,sha256=hJ-6VVsWczzKDH6fQRN1WmYhcitixuXJ-eNOuCo_JgM,2742
|
169
170
|
piccolo/query/methods/create_index.py,sha256=gip_cRXZkLfpJqCL7KHk2l_7HLptoa-Ae8qu6I5d5c8,2224
|
@@ -195,17 +196,17 @@ piccolo/utils/printing.py,sha256=5VWNSfOrIGPh1VM-7fd4K18RGCYk0FQ5o-D4aLhzXZE,174
|
|
195
196
|
piccolo/utils/pydantic.py,sha256=RhoQZ7ddmFmepVcslHXMqmynbSVch7XLKUSgJkLuQS0,12327
|
196
197
|
piccolo/utils/repr.py,sha256=K3w-TAP9WPx8tbAIB2XDab_C4PHsPrB9TzwWfOHa4cc,787
|
197
198
|
piccolo/utils/sql_values.py,sha256=pGXmVTw6pWr8q7QA4xs7NiKSwjBzhN--3HXVjQv2SQQ,1749
|
198
|
-
piccolo/utils/sync.py,sha256=
|
199
|
+
piccolo/utils/sync.py,sha256=irmdTsYtURZNEBmIP6i2v2PyjgE-gK4t6VSxnBZ75Qo,920
|
199
200
|
piccolo/utils/warnings.py,sha256=ONrurw3HVCClUuHnpenMjg45dcFesrXqMgG9ifgP4_8,1247
|
200
201
|
piccolo/utils/graphlib/__init__.py,sha256=SUJ5Yh7LiRun3nkBuLUSVmGNHF6fANrxSoYan0mtYB0,200
|
201
202
|
piccolo/utils/graphlib/_graphlib.py,sha256=9FNGDSmTIEAk86FktniCe_J2yXjSE_sRZHDBAJJAUOw,9677
|
202
|
-
piccolo-1.
|
203
|
+
piccolo-1.26.0.dist-info/licenses/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
|
203
204
|
profiling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
204
205
|
profiling/run_profile.py,sha256=264qsSFu93NTpExePnKQ9GkcN5fiuRBQ72WOSt0ZHck,829
|
205
206
|
tests/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
206
207
|
tests/apps/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
207
208
|
tests/apps/app/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
208
|
-
tests/apps/app/commands/test_new.py,sha256=
|
209
|
+
tests/apps/app/commands/test_new.py,sha256=Z4Vt3G60sVw6Ab7npn3PS5daq5BTWEzlHu7LcZIorfg,2194
|
209
210
|
tests/apps/app/commands/test_show_all.py,sha256=ca1afRhePYyVw-x2swxnoj659_qdu53uY8sVGIqBuNg,551
|
210
211
|
tests/apps/asgi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
211
212
|
tests/apps/asgi/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -221,7 +222,7 @@ tests/apps/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
221
222
|
tests/apps/migrations/test_migration.py,sha256=JmPLtf2BCWX3Yofe0GQe40m8I_yWa_-3vk1lDfFDfIo,308
|
222
223
|
tests/apps/migrations/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
223
224
|
tests/apps/migrations/auto/test_diffable_table.py,sha256=p0cKDkfhmu96-rB9bonOlg5bmfQ7U9S2kRppOt4YxyU,3338
|
224
|
-
tests/apps/migrations/auto/test_migration_manager.py,sha256=
|
225
|
+
tests/apps/migrations/auto/test_migration_manager.py,sha256=qzYHC8nKm4pRtsgIyPDSZdXEWLOl2UDgTv6Mv8QO49k,37362
|
225
226
|
tests/apps/migrations/auto/test_schema_differ.py,sha256=UdsaZisA02j15wr1bXkXD6Cqu3p0A23NwFQLXsJdQL4,19391
|
226
227
|
tests/apps/migrations/auto/test_schema_snapshot.py,sha256=ZyvGZqn3N3cwd-3S-FME5AJ8buDSHesw7yPIvY6mE5k,6196
|
227
228
|
tests/apps/migrations/auto/test_serialisation.py,sha256=EFkhES1w9h51UCamWrhxs3mf4I718ggeP7Yl5J_UID4,13548
|
@@ -288,7 +289,7 @@ tests/columns/m2m/test_m2m.py,sha256=0ObmIHUJF6CZoNBznc5xXVr5_BbGBqOmWwtpg8IcPt4
|
|
288
289
|
tests/columns/m2m/test_m2m_schema.py,sha256=oxu7eAjFFpDjnq9Eq-5OTNmlnsEIMFWx18OItfpVs-s,339
|
289
290
|
tests/conf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
290
291
|
tests/conf/example.py,sha256=K8sTttLpEac8rQlOLDY500IGkHj3P3NoyFbCMnT1EqY,347
|
291
|
-
tests/conf/test_apps.py,sha256=
|
292
|
+
tests/conf/test_apps.py,sha256=5Cl9zL_nJr5NDS7KiTIIne219RGSouxP2gWhQK99F7A,9925
|
292
293
|
tests/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
293
294
|
tests/engine/test_extra_nodes.py,sha256=xW5gflHzM6ou26DqRSAZoaAbYVzF1IuMkW3vzNmB954,1298
|
294
295
|
tests/engine/test_logging.py,sha256=VLf9A3QuoV7OhV8lttLDB4gzZemnG63kSr-Uyan005U,1287
|
@@ -361,7 +362,7 @@ tests/table/instance/test_create.py,sha256=JD0l7L9dDK1FKPhUs6WC_B2bruPR1qQ8aIqXp
|
|
361
362
|
tests/table/instance/test_get_related.py,sha256=eracFunh4Qlj5BEkI7OsrOyefRZM0rxrXnFX92VL1ZE,3285
|
362
363
|
tests/table/instance/test_get_related_readable.py,sha256=QDMMZykxPsTWcsl8ZIZtmQVLwSGCw7QBilLepAAAnWg,4694
|
363
364
|
tests/table/instance/test_instantiate.py,sha256=jvtaqSa_zN1lHQiykN4EnwitZqkWAbXle5IJtyhKuHY,958
|
364
|
-
tests/table/instance/test_remove.py,sha256=
|
365
|
+
tests/table/instance/test_remove.py,sha256=P-8wWodlA2PukdWhaj8x2HYZI1U8Q7oIuFBe2sAkcdo,757
|
365
366
|
tests/table/instance/test_save.py,sha256=ccdDz-bR3aYDa16_RGQP7JTXprgm1mT6-NpF1y3RXyo,4388
|
366
367
|
tests/table/instance/test_to_dict.py,sha256=gkiYkmcI5qcy5E-ERWWmO-Q8uyVSFfcpJ8d53LlzCuI,3442
|
367
368
|
tests/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -380,8 +381,8 @@ tests/utils/test_sql_values.py,sha256=vzxRmy16FfLZPH-sAQexBvsF9MXB8n4smr14qoEOS5
|
|
380
381
|
tests/utils/test_sync.py,sha256=9ytVo56y2vPQePvTeIi9lHIouEhWJbodl1TmzkGFrSo,799
|
381
382
|
tests/utils/test_table_reflection.py,sha256=SIzuat-IpcVj1GCFyOWKShI8YkhdOPPFH7qVrvfyPNE,3794
|
382
383
|
tests/utils/test_warnings.py,sha256=NvSC_cvJ6uZcwAGf1m-hLzETXCqprXELL8zg3TNLVMw,269
|
383
|
-
piccolo-1.
|
384
|
-
piccolo-1.
|
385
|
-
piccolo-1.
|
386
|
-
piccolo-1.
|
387
|
-
piccolo-1.
|
384
|
+
piccolo-1.26.0.dist-info/METADATA,sha256=UE7oErMiVueu8PQJLKAzHWoVIaXJtA2r9d1VZsOzwbU,5531
|
385
|
+
piccolo-1.26.0.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
386
|
+
piccolo-1.26.0.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
|
387
|
+
piccolo-1.26.0.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
|
388
|
+
piccolo-1.26.0.dist-info/RECORD,,
|
@@ -3,7 +3,12 @@ import shutil
|
|
3
3
|
import tempfile
|
4
4
|
from unittest import TestCase
|
5
5
|
|
6
|
-
from piccolo.apps.app.commands.new import
|
6
|
+
from piccolo.apps.app.commands.new import (
|
7
|
+
get_app_module,
|
8
|
+
module_exists,
|
9
|
+
new,
|
10
|
+
validate_app_name,
|
11
|
+
)
|
7
12
|
|
8
13
|
|
9
14
|
class TestModuleExists(TestCase):
|
@@ -43,3 +48,37 @@ class TestNewApp(TestCase):
|
|
43
48
|
"A module called sys already exists"
|
44
49
|
)
|
45
50
|
)
|
51
|
+
|
52
|
+
|
53
|
+
class TestValidateAppName(TestCase):
|
54
|
+
|
55
|
+
def test_validate_app_name(self):
|
56
|
+
"""
|
57
|
+
Make sure only app names which work as valid Python package names are
|
58
|
+
allowed.
|
59
|
+
"""
|
60
|
+
# Should be rejected:
|
61
|
+
for app_name in ("MY APP", "app/my_app", "my.app"):
|
62
|
+
with self.assertRaises(ValueError):
|
63
|
+
validate_app_name(app_name=app_name)
|
64
|
+
|
65
|
+
# Should work fine:
|
66
|
+
validate_app_name(app_name="music")
|
67
|
+
|
68
|
+
|
69
|
+
class TestGetAppIdentifier(TestCase):
|
70
|
+
|
71
|
+
def test_get_app_module(self):
|
72
|
+
"""
|
73
|
+
Make sure the the ``root`` argument is handled correctly.
|
74
|
+
"""
|
75
|
+
self.assertEqual(
|
76
|
+
get_app_module(app_name="music", root="."),
|
77
|
+
"music.piccolo_app",
|
78
|
+
)
|
79
|
+
|
80
|
+
for root in ("apps", "./apps", "./apps/"):
|
81
|
+
self.assertEqual(
|
82
|
+
get_app_module(app_name="music", root=root),
|
83
|
+
"apps.music.piccolo_app",
|
84
|
+
)
|
@@ -12,8 +12,10 @@ from piccolo.columns.base import OnDelete, OnUpdate
|
|
12
12
|
from piccolo.columns.column_types import ForeignKey
|
13
13
|
from piccolo.conf.apps import AppConfig
|
14
14
|
from piccolo.engine import engine_finder
|
15
|
+
from piccolo.query.constraints import get_fk_constraint_rules
|
15
16
|
from piccolo.table import Table, sort_table_classes
|
16
17
|
from piccolo.utils.lazy_loader import LazyLoader
|
18
|
+
from piccolo.utils.sync import run_sync
|
17
19
|
from tests.base import AsyncMock, DBTestCase, engine_is, engines_only
|
18
20
|
from tests.example_apps.music.tables import Band, Concert, Manager, Venue
|
19
21
|
|
@@ -618,6 +620,54 @@ class TestMigrationManager(DBTestCase):
|
|
618
620
|
response = self.run_sync("SELECT * FROM manager;")
|
619
621
|
self.assertEqual(response, [{"id": id[0]["id"], "name": "Dave"}])
|
620
622
|
|
623
|
+
@engines_only("postgres", "cockroach")
|
624
|
+
def test_alter_fk_on_delete_on_update(self):
|
625
|
+
"""
|
626
|
+
Test altering OnDelete and OnUpdate with MigrationManager.
|
627
|
+
"""
|
628
|
+
# before performing migrations - OnDelete.no_action
|
629
|
+
self.assertEqual(
|
630
|
+
run_sync(get_fk_constraint_rules(column=Band.manager)).on_delete,
|
631
|
+
OnDelete.no_action,
|
632
|
+
)
|
633
|
+
|
634
|
+
manager = MigrationManager(app_name="music")
|
635
|
+
manager.alter_column(
|
636
|
+
table_class_name="Band",
|
637
|
+
tablename="band",
|
638
|
+
column_name="manager",
|
639
|
+
db_column_name="manager",
|
640
|
+
params={
|
641
|
+
"on_delete": OnDelete.set_null,
|
642
|
+
"on_update": OnUpdate.set_null,
|
643
|
+
},
|
644
|
+
old_params={
|
645
|
+
"on_delete": OnDelete.no_action,
|
646
|
+
"on_update": OnUpdate.no_action,
|
647
|
+
},
|
648
|
+
column_class=ForeignKey,
|
649
|
+
old_column_class=ForeignKey,
|
650
|
+
schema=None,
|
651
|
+
)
|
652
|
+
|
653
|
+
asyncio.run(manager.run())
|
654
|
+
|
655
|
+
# after performing migrations - OnDelete.set_null
|
656
|
+
self.assertEqual(
|
657
|
+
run_sync(get_fk_constraint_rules(column=Band.manager)).on_delete,
|
658
|
+
OnDelete.set_null,
|
659
|
+
)
|
660
|
+
|
661
|
+
# Reverse
|
662
|
+
asyncio.run(manager.run(backwards=True))
|
663
|
+
|
664
|
+
# after performing reverse migrations we have
|
665
|
+
# OnDelete.no_action again
|
666
|
+
self.assertEqual(
|
667
|
+
run_sync(get_fk_constraint_rules(column=Band.manager)).on_delete,
|
668
|
+
OnDelete.no_action,
|
669
|
+
)
|
670
|
+
|
621
671
|
@engines_only("postgres")
|
622
672
|
def test_alter_column_unique(self):
|
623
673
|
"""
|
tests/conf/test_apps.py
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import pathlib
|
4
|
+
import tempfile
|
4
5
|
from unittest import TestCase
|
5
6
|
|
6
7
|
from piccolo.apps.user.tables import BaseUser
|
7
|
-
from piccolo.conf.apps import
|
8
|
+
from piccolo.conf.apps import (
|
9
|
+
AppConfig,
|
10
|
+
AppRegistry,
|
11
|
+
Finder,
|
12
|
+
PiccoloConfUpdater,
|
13
|
+
table_finder,
|
14
|
+
)
|
8
15
|
from tests.example_apps.mega.tables import MegaTable, SmallTable
|
9
16
|
from tests.example_apps.music.tables import (
|
10
17
|
Band,
|
@@ -310,3 +317,44 @@ class TestFinder(TestCase):
|
|
310
317
|
self.assertListEqual(
|
311
318
|
[i.app_name for i in sorted_app_configs], ["app_2", "app_1"]
|
312
319
|
)
|
320
|
+
|
321
|
+
|
322
|
+
class TestPiccoloConfUpdater(TestCase):
|
323
|
+
|
324
|
+
def test_modify_app_registry_src(self):
|
325
|
+
"""
|
326
|
+
Make sure the `piccolo_conf.py` source code can be modified
|
327
|
+
successfully.
|
328
|
+
"""
|
329
|
+
updater = PiccoloConfUpdater()
|
330
|
+
|
331
|
+
new_src = updater._modify_app_registry_src(
|
332
|
+
src="APP_REGISTRY = AppRegistry(apps=[])",
|
333
|
+
app_module="music.piccolo_app",
|
334
|
+
)
|
335
|
+
self.assertEqual(
|
336
|
+
new_src.strip(),
|
337
|
+
'APP_REGISTRY = AppRegistry(apps=["music.piccolo_app"])',
|
338
|
+
)
|
339
|
+
|
340
|
+
def test_register_app(self):
|
341
|
+
"""
|
342
|
+
Make sure the new contents get written to disk.
|
343
|
+
"""
|
344
|
+
temp_dir = tempfile.gettempdir()
|
345
|
+
piccolo_conf_path = pathlib.Path(temp_dir) / "piccolo_conf.py"
|
346
|
+
|
347
|
+
src = "APP_REGISTRY = AppRegistry(apps=[])"
|
348
|
+
|
349
|
+
with open(piccolo_conf_path, "wt") as f:
|
350
|
+
f.write(src)
|
351
|
+
|
352
|
+
updater = PiccoloConfUpdater(piccolo_conf_path=str(piccolo_conf_path))
|
353
|
+
updater.register_app(app_module="music.piccolo_app")
|
354
|
+
|
355
|
+
with open(piccolo_conf_path) as f:
|
356
|
+
contents = f.read().strip()
|
357
|
+
|
358
|
+
self.assertEqual(
|
359
|
+
contents, 'APP_REGISTRY = AppRegistry(apps=["music.piccolo_app"])'
|
360
|
+
)
|
@@ -17,9 +17,11 @@ class TestRemove(TestCase):
|
|
17
17
|
"Maz"
|
18
18
|
in Manager.select(Manager.name).output(as_list=True).run_sync()
|
19
19
|
)
|
20
|
+
self.assertEqual(manager._exists_in_db, True)
|
20
21
|
|
21
22
|
manager.remove().run_sync()
|
22
23
|
self.assertTrue(
|
23
24
|
"Maz"
|
24
25
|
not in Manager.select(Manager.name).output(as_list=True).run_sync()
|
25
26
|
)
|
27
|
+
self.assertEqual(manager._exists_in_db, False)
|
File without changes
|
File without changes
|
File without changes
|