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 CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "1.24.2"
1
+ __VERSION__ = "1.26.0"
@@ -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
- def new_app(app_name: str, root: str = "."):
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
- if os.path.exists(app_root):
39
- sys.exit("Folder already exists - exiting.")
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.mkdir(app_root)
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. /my/folder. By default it creates the
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=["{{ app_name }}.tables"],
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 not in the preview mode,
427
- executes the queries. else, prints the query.
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"] = ONUPDATE_MAP[trigger.on_update]
769
- kwargs["on_delete"] = ONDELETE_MAP[trigger.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']``. The path should be from the root of your project,
50
- not a relative path.
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(module_path)
108
+ module = import_module(
109
+ module_path,
110
+ package=package,
111
+ )
88
112
  except ImportError as exception:
89
- print(f"Unable to import {module_path}")
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__ != module_path:
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
+ )
@@ -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}_fk"
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
- return self.drop_constraint(constraint_name=constraint_name)
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(column=column)
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
@@ -573,6 +573,8 @@ class Table(metaclass=TableMetaclass):
573
573
 
574
574
  setattr(self, self._meta.primary_key._meta.name, None)
575
575
 
576
+ self._exists_in_db = False
577
+
576
578
  return self.__class__.delete().where(
577
579
  self.__class__._meta.primary_key == primary_key_value
578
580
  )
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
- def run_sync(coroutine: t.Coroutine):
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piccolo
3
- Version: 1.24.2
3
+ Version: 1.26.0
4
4
  Summary: A fast, user friendly ORM and query builder which supports asyncio.
5
5
  Home-page: https://github.com/piccolo-orm/piccolo
6
6
  Author: Daniel Townsend
@@ -1,18 +1,18 @@
1
- piccolo/__init__.py,sha256=53bY15Zwtb-mVcNomjadBbUceria081xCDSw7iTXiZM,23
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=UvEbagMYRkTbyFHTUwUshZlL_dC4UKDP7vUOwF8OXmg,50593
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=odvFHBiKmbgmlfDXxMcU-0qlX8LumRdvPGzfX-SkMWI,2187
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=Wl11z0J0EiT5kDFJ_3Xps0dn-qndloDmAs1U_7UB1BY,554
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=nTzXc5IJLl_al1FuzG5AnaA1vSn-ipMurpPK7BibmB8,2710
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=tyAE1Xk6Xb58igbuHw3FGHhKkVjv_Vr5qBH4AbvVb8k,35856
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=_niW_UxZ-x29-xjDIVdjHdDyKAaXn4HUDVtVjsXgBD0,30700
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=AlgOM2ePl9NQjME5AQxcZ5RbuQpjRywFCj5uHUsCIZU,19040
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=AI9YkJeip2EitrWJN_TDExXhA8HGAG3XuDz1NR-KirQ,16728
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=j9Abkxn5HHS6HyvfpMzb1zV_teTkFHVhaIxu9rrSwSU,819
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.24.2.dist-info/licenses/LICENSE,sha256=zFIpi-16uIJ420UMIG75NU0JbDBykvrdnXcj5U_EYBI,1059
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=GxVAgyqEtbuHCUWaLhTzTUOxfqQ1daFq8SgdIcWjr_8,1187
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=L2raSuhZybRyK-BEh-G-nDndAgl4sLynekHQFqyrteY,35719
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=Ovdp4v55iC-epS25sKntyYAw2ki9svcyCNOj5rOzE-E,8632
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=Zv22ZZqot61rjCVWL1PHDf1oxELcBnmMXx1gsST6j80,648
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.24.2.dist-info/METADATA,sha256=zrjcB9J7oo9wGk6Z19Xgpm6Iupa_8VymJ9OOpIUMvA8,5531
384
- piccolo-1.24.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
385
- piccolo-1.24.2.dist-info/entry_points.txt,sha256=SJPHET4Fi1bN5F3WqcKkv9SClK3_F1I7m4eQjk6AFh0,46
386
- piccolo-1.24.2.dist-info/top_level.txt,sha256=-SR74VGbk43VoPy1HH-mHm97yoGukLK87HE5kdBW6qM,24
387
- piccolo-1.24.2.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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 module_exists, new
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 AppConfig, AppRegistry, Finder, table_finder
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)