ormlambda 3.7.2__tar.gz → 3.11.1__tar.gz

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.
Files changed (125) hide show
  1. {ormlambda-3.7.2 → ormlambda-3.11.1}/PKG-INFO +107 -8
  2. {ormlambda-3.7.2 → ormlambda-3.11.1}/README.md +106 -5
  3. {ormlambda-3.7.2 → ormlambda-3.11.1}/pyproject.toml +3 -3
  4. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/__init__.py +2 -0
  5. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/caster.py +1 -0
  6. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/__init__.py +1 -0
  7. ormlambda-3.11.1/src/ormlambda/databases/my_sql/clauses/alias.py +28 -0
  8. ormlambda-3.11.1/src/ormlambda/databases/my_sql/clauses/group_by.py +31 -0
  9. ormlambda-3.11.1/src/ormlambda/databases/my_sql/clauses/having.py +16 -0
  10. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/order.py +6 -1
  11. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/where.py +3 -3
  12. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/functions/concat.py +8 -6
  13. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/functions/max.py +1 -1
  14. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/join_context.py +3 -2
  15. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/repository/repository.py +57 -11
  16. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/statements.py +83 -31
  17. ormlambda-3.11.1/src/ormlambda/engine/__init__.py +2 -0
  18. ormlambda-3.11.1/src/ormlambda/engine/create.py +35 -0
  19. ormlambda-3.11.1/src/ormlambda/engine/url.py +744 -0
  20. ormlambda-3.11.1/src/ormlambda/engine/utils.py +17 -0
  21. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/repository/base_repository.py +0 -1
  22. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/column.py +27 -2
  23. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/foreign_key.py +36 -4
  24. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/statements/interfaces/IStatements.py +49 -42
  25. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/statements/types.py +9 -3
  26. ormlambda-3.7.2/src/ormlambda/databases/my_sql/clauses/alias.py +0 -34
  27. ormlambda-3.7.2/src/ormlambda/databases/my_sql/clauses/group_by.py +0 -32
  28. ormlambda-3.7.2/src/ormlambda/sql/interfaces/__init__.py +0 -0
  29. {ormlambda-3.7.2 → ormlambda-3.11.1}/LICENSE +0 -0
  30. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/caster/__init__.py +0 -0
  31. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/caster/base_caster.py +0 -0
  32. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/caster/caster.py +0 -0
  33. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/caster/interfaces/ICaster.py +0 -0
  34. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/caster/interfaces/__init__.py +0 -0
  35. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/__init__.py +0 -0
  36. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/abstract_classes/__init__.py +0 -0
  37. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/abstract_classes/decomposition_query.py +0 -0
  38. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/abstract_classes/non_query_base.py +0 -0
  39. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/abstract_classes/query_base.py +0 -0
  40. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/enums/__init__.py +0 -0
  41. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/enums/condition_types.py +0 -0
  42. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/enums/join_type.py +0 -0
  43. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/errors/__init__.py +0 -0
  44. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/global_checker.py +0 -0
  45. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/ICustomAlias.py +0 -0
  46. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/IDecompositionQuery.py +0 -0
  47. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/IJoinSelector.py +0 -0
  48. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/INonQueryCommand.py +0 -0
  49. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/IQueryCommand.py +0 -0
  50. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/common/interfaces/__init__.py +0 -0
  51. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/__init__.py +0 -0
  52. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/delete/IDelete.py +0 -0
  53. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/delete/__init__.py +0 -0
  54. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/delete/abstract_delete.py +0 -0
  55. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/insert/IInsert.py +0 -0
  56. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/insert/__init__.py +0 -0
  57. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/insert/abstract_insert.py +0 -0
  58. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/select/ISelect.py +0 -0
  59. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/select/__init__.py +0 -0
  60. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/update/IUpdate.py +0 -0
  61. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/update/__init__.py +0 -0
  62. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/update/abstract_update.py +0 -0
  63. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/upsert/IUpsert.py +0 -0
  64. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/upsert/__init__.py +0 -0
  65. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/components/upsert/abstract_upsert.py +0 -0
  66. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/__init__.py +0 -0
  67. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/__init__.py +0 -0
  68. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/__init__.py +0 -0
  69. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/read.py +0 -0
  70. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/__init__.py +0 -0
  71. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/bytes.py +0 -0
  72. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/datetime.py +0 -0
  73. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/float.py +0 -0
  74. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/int.py +0 -0
  75. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/iterable.py +0 -0
  76. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/none.py +0 -0
  77. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/point.py +0 -0
  78. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/types/string.py +0 -0
  79. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/caster/write.py +0 -0
  80. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/ST_AsText.py +0 -0
  81. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/ST_Contains.py +0 -0
  82. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/count.py +0 -0
  83. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/create_database.py +0 -0
  84. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/delete.py +0 -0
  85. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/drop_database.py +0 -0
  86. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/drop_table.py +0 -0
  87. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/insert.py +0 -0
  88. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/joins.py +0 -0
  89. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/limit.py +0 -0
  90. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/offset.py +0 -0
  91. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/select.py +0 -0
  92. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/update.py +0 -0
  93. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/clauses/upsert.py +0 -0
  94. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/functions/__init__.py +0 -0
  95. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/functions/min.py +0 -0
  96. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/functions/sum.py +0 -0
  97. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/repository/__init__.py +0 -0
  98. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/databases/my_sql/types.py +0 -0
  99. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/engine/template.py +0 -0
  100. {ormlambda-3.7.2/src/ormlambda/engine → ormlambda-3.11.1/src/ormlambda/model}/__init__.py +0 -0
  101. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/model/base_model.py +0 -0
  102. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/repository/__init__.py +0 -0
  103. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/repository/interfaces/IDatabaseConnection.py +0 -0
  104. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/repository/interfaces/IRepositoryBase.py +0 -0
  105. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/repository/interfaces/__init__.py +0 -0
  106. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/__init__.py +0 -0
  107. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/clause_info/__init__.py +0 -0
  108. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/clause_info/clause_info.py +0 -0
  109. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/clause_info/clause_info_context.py +0 -0
  110. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/clause_info/interface/IAggregate.py +0 -0
  111. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/clause_info/interface/__init__.py +0 -0
  112. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/comparer.py +0 -0
  113. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/dtypes.py +0 -0
  114. {ormlambda-3.7.2/src/ormlambda/model → ormlambda-3.11.1/src/ormlambda/sql/interfaces}/__init__.py +0 -0
  115. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/table/__init__.py +0 -0
  116. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/table/fields.py +0 -0
  117. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/table/table_constructor.py +0 -0
  118. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/sql/types.py +0 -0
  119. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/statements/__init__.py +0 -0
  120. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/statements/base_statement.py +0 -0
  121. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/statements/interfaces/__init__.py +0 -0
  122. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/utils/__init__.py +0 -0
  123. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/utils/module_tree/__init__.py +0 -0
  124. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/utils/module_tree/dfs_traversal.py +0 -0
  125. {ormlambda-3.7.2 → ormlambda-3.11.1}/src/ormlambda/utils/module_tree/dynamic_module.py +0 -0
@@ -1,15 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ormlambda
3
- Version: 3.7.2
3
+ Version: 3.11.1
4
4
  Summary: ORM designed to interact with the database (currently with MySQL) using lambda functions and nested functions
5
5
  Author: p-hzamora
6
6
  Author-email: p.hzamora@icloud.com
7
7
  Requires-Python: >=3.12,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
- Requires-Dist: fluent-validation (==4.3.1)
11
10
  Requires-Dist: mysql-connector-python (>=9.0.0,<10.0.0)
12
- Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
13
11
  Requires-Dist: shapely (>=2.0.6,<3.0.0)
14
12
  Description-Content-Type: text/markdown
15
13
 
@@ -17,7 +15,7 @@ Description-Content-Type: text/markdown
17
15
  ![downloads](https://img.shields.io/pypi/dm/ormlambda?label=downloads)
18
16
  ![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)
19
17
 
20
- # ormMySQL
18
+ # ormlambda Documentation
21
19
  This ORM is designed to connect with a MySQL server, facilitating the management of various database queries. Built with flexibility and efficiency in mind, this ORM empowers developers to interact with the database using lambda functions, allowing for concise and expressive query construction.
22
20
 
23
21
  # Creating your first lambda query
@@ -38,19 +36,34 @@ database = MySQLRepository(user=USERNAME, password=PASSWORD, database="sakila",
38
36
  ## Select all columns
39
37
  ```python
40
38
  from ormlambda import ORM
41
- from ormlambda.databases.my_sql import MySQLRepository
39
+ from ormlambda import create_engine
42
40
 
43
41
  from models.address import Address
44
- from config import config_dict
42
+ from test.config import config_dict
45
43
 
46
- db = MySQLRepository(**config_dict)
44
+ db = create_engine('mysql://root:1234@localhost:3306/sakila')
47
45
 
48
- AddressModel = ORM(Address,db)
46
+ AddressModel = ORM(Address, db)
49
47
 
50
48
  result = AddressModel.select()
51
49
  ```
52
50
  The `result` var will be of type `tuple[Address, ...]`
53
51
 
52
+ ## Improving Typing
53
+ For those cases where you need to pass the database configuration from a `dict`, you can use `MySQLArgs` TypedDict object to improve type annotations.
54
+
55
+ ```python
56
+ from ormlambda.databases.my_sql.types import MySQLArgs
57
+
58
+ config_dict: MySQLArgs = {
59
+ "user": DB_USERNAME,
60
+ "password": DB_PASSWORD,
61
+ "host": DB_HOST,
62
+ "database": DB_DATABASE,
63
+ }
64
+ db = MySQLRepository(**config_dict)
65
+ ```
66
+
54
67
  ## Select multiples tables
55
68
  Once the `AddressModel` class is created, we will not only be able to access all the information in that table, but also all the information in all the tables that have foreign keys related to it."
56
69
 
@@ -296,6 +309,92 @@ res = AddressModel.sum(Address.address_id, execute=True)
296
309
  res = AddressModel.count(Address.address_id, execute=True)
297
310
  ```
298
311
 
312
+ ## 1. Concat
313
+
314
+ The `concat` method allows you to concatenate multiple columns or values into a single string. This is particularly useful for creating derived fields in your queries.
315
+
316
+ #### Usage
317
+
318
+ ```python
319
+ response = ORM(Address, db).where(Address.City.Country.country.regex(r"^Spain")).first(
320
+ (
321
+ Address.address,
322
+ Address.City.city,
323
+ self.tmodel.concat(
324
+ (
325
+ "Address: ",
326
+ Address.address,
327
+ " - city: ",
328
+ Address.City.city,
329
+ " - country: ",
330
+ Address.City.Country.country,
331
+ )
332
+ ),
333
+ ),
334
+ flavour=dict,
335
+ )
336
+
337
+ {
338
+ "address_address": "939 Probolinggo Loop",
339
+ "city_city": "A Coruña (La Coruña)",
340
+ "CONCAT": "Address: 939 Probolinggo Loop - city: A Coruña (La Coruña) - country: Spain",
341
+ }
342
+ ```
343
+ As you can see in the response, the result is a dictionary where the keys are a combination of the table name and the column name. This is done to avoid collisions with columns from other tables that might have the same name.
344
+
345
+ Another elegant approach to adjust the response and obtain an object is by using the `flavour` attribute. You can pass a callable object, which will be used to instantiate it with the returned data.
346
+
347
+ ## Using BaseModel for Custom Responses (Pydantic)
348
+
349
+ You can utilize `BaseModel` from Pydantic to create structured response models. This allows you to define the expected structure of your data, ensuring type safety and validation.
350
+
351
+ ### Example: Creating a Custom Response Model
352
+
353
+ You can create a custom response model by subclassing `BaseModel`. In this model, you define the fields that you expect in your response, along with their types.
354
+
355
+ ```python
356
+ class AddressCombine(BaseModel):
357
+ address: str
358
+ city: str
359
+ country: str
360
+
361
+ model_config: ConfigDict = {"extra": "forbid"}
362
+
363
+ ddbb = MySQLRepository(**config_dict)
364
+ model = ORM(Address, ddbb)
365
+ select = (
366
+ model.order(lambda x: x.City.Country.country, "DESC")
367
+ .limit(10)
368
+ .where(Address.City.Country.country == "Spain")
369
+ .first(
370
+ lambda x: (
371
+ x.address,
372
+ x.City.city,
373
+ x.City.Country.country,
374
+ ),
375
+ flavour=AddressCombine,
376
+ )
377
+ )
378
+ ```
379
+
380
+ Once you execute the query, the result will be an instance of your custom model. You can access the fields directly, ensuring that the data adheres to the structure you defined.
381
+
382
+ ```python
383
+
384
+
385
+ print(select.address)
386
+ print(select.city)
387
+ print(select.country)
388
+ ```
389
+
390
+
391
+ <!-- ### 2. Having
392
+
393
+ The `having` method is used to filter results based on aggregate functions. It is typically used in conjunction with `group by` clauses.
394
+
395
+ #### Usage -->
396
+
397
+
299
398
  ## Combine aggregation method
300
399
  As shown in the previous examples, setting the `execute` attribute to `True` allows us to perform the corresponding query in a single line. However, if you're looking to improve efficiency, you can combine all of them into one query.
301
400
  ```python
@@ -2,7 +2,7 @@
2
2
  ![downloads](https://img.shields.io/pypi/dm/ormlambda?label=downloads)
3
3
  ![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)
4
4
 
5
- # ormMySQL
5
+ # ormlambda Documentation
6
6
  This ORM is designed to connect with a MySQL server, facilitating the management of various database queries. Built with flexibility and efficiency in mind, this ORM empowers developers to interact with the database using lambda functions, allowing for concise and expressive query construction.
7
7
 
8
8
  # Creating your first lambda query
@@ -23,19 +23,34 @@ database = MySQLRepository(user=USERNAME, password=PASSWORD, database="sakila",
23
23
  ## Select all columns
24
24
  ```python
25
25
  from ormlambda import ORM
26
- from ormlambda.databases.my_sql import MySQLRepository
26
+ from ormlambda import create_engine
27
27
 
28
28
  from models.address import Address
29
- from config import config_dict
29
+ from test.config import config_dict
30
30
 
31
- db = MySQLRepository(**config_dict)
31
+ db = create_engine('mysql://root:1234@localhost:3306/sakila')
32
32
 
33
- AddressModel = ORM(Address,db)
33
+ AddressModel = ORM(Address, db)
34
34
 
35
35
  result = AddressModel.select()
36
36
  ```
37
37
  The `result` var will be of type `tuple[Address, ...]`
38
38
 
39
+ ## Improving Typing
40
+ For those cases where you need to pass the database configuration from a `dict`, you can use `MySQLArgs` TypedDict object to improve type annotations.
41
+
42
+ ```python
43
+ from ormlambda.databases.my_sql.types import MySQLArgs
44
+
45
+ config_dict: MySQLArgs = {
46
+ "user": DB_USERNAME,
47
+ "password": DB_PASSWORD,
48
+ "host": DB_HOST,
49
+ "database": DB_DATABASE,
50
+ }
51
+ db = MySQLRepository(**config_dict)
52
+ ```
53
+
39
54
  ## Select multiples tables
40
55
  Once the `AddressModel` class is created, we will not only be able to access all the information in that table, but also all the information in all the tables that have foreign keys related to it."
41
56
 
@@ -281,6 +296,92 @@ res = AddressModel.sum(Address.address_id, execute=True)
281
296
  res = AddressModel.count(Address.address_id, execute=True)
282
297
  ```
283
298
 
299
+ ## 1. Concat
300
+
301
+ The `concat` method allows you to concatenate multiple columns or values into a single string. This is particularly useful for creating derived fields in your queries.
302
+
303
+ #### Usage
304
+
305
+ ```python
306
+ response = ORM(Address, db).where(Address.City.Country.country.regex(r"^Spain")).first(
307
+ (
308
+ Address.address,
309
+ Address.City.city,
310
+ self.tmodel.concat(
311
+ (
312
+ "Address: ",
313
+ Address.address,
314
+ " - city: ",
315
+ Address.City.city,
316
+ " - country: ",
317
+ Address.City.Country.country,
318
+ )
319
+ ),
320
+ ),
321
+ flavour=dict,
322
+ )
323
+
324
+ {
325
+ "address_address": "939 Probolinggo Loop",
326
+ "city_city": "A Coruña (La Coruña)",
327
+ "CONCAT": "Address: 939 Probolinggo Loop - city: A Coruña (La Coruña) - country: Spain",
328
+ }
329
+ ```
330
+ As you can see in the response, the result is a dictionary where the keys are a combination of the table name and the column name. This is done to avoid collisions with columns from other tables that might have the same name.
331
+
332
+ Another elegant approach to adjust the response and obtain an object is by using the `flavour` attribute. You can pass a callable object, which will be used to instantiate it with the returned data.
333
+
334
+ ## Using BaseModel for Custom Responses (Pydantic)
335
+
336
+ You can utilize `BaseModel` from Pydantic to create structured response models. This allows you to define the expected structure of your data, ensuring type safety and validation.
337
+
338
+ ### Example: Creating a Custom Response Model
339
+
340
+ You can create a custom response model by subclassing `BaseModel`. In this model, you define the fields that you expect in your response, along with their types.
341
+
342
+ ```python
343
+ class AddressCombine(BaseModel):
344
+ address: str
345
+ city: str
346
+ country: str
347
+
348
+ model_config: ConfigDict = {"extra": "forbid"}
349
+
350
+ ddbb = MySQLRepository(**config_dict)
351
+ model = ORM(Address, ddbb)
352
+ select = (
353
+ model.order(lambda x: x.City.Country.country, "DESC")
354
+ .limit(10)
355
+ .where(Address.City.Country.country == "Spain")
356
+ .first(
357
+ lambda x: (
358
+ x.address,
359
+ x.City.city,
360
+ x.City.Country.country,
361
+ ),
362
+ flavour=AddressCombine,
363
+ )
364
+ )
365
+ ```
366
+
367
+ Once you execute the query, the result will be an instance of your custom model. You can access the fields directly, ensuring that the data adheres to the structure you defined.
368
+
369
+ ```python
370
+
371
+
372
+ print(select.address)
373
+ print(select.city)
374
+ print(select.country)
375
+ ```
376
+
377
+
378
+ <!-- ### 2. Having
379
+
380
+ The `having` method is used to filter results based on aggregate functions. It is typically used in conjunction with `group by` clauses.
381
+
382
+ #### Usage -->
383
+
384
+
284
385
  ## Combine aggregation method
285
386
  As shown in the previous examples, setting the `execute` attribute to `True` allows us to perform the corresponding query in a single line. However, if you're looking to improve efficiency, you can combine all of them into one query.
286
387
  ```python
@@ -3,7 +3,7 @@ line-length = 320
3
3
 
4
4
  [tool.poetry]
5
5
  name = "ormlambda"
6
- version = "3.7.2"
6
+ version = "3.11.1"
7
7
  description = "ORM designed to interact with the database (currently with MySQL) using lambda functions and nested functions"
8
8
  authors = ["p-hzamora <p.hzamora@icloud.com>"]
9
9
  readme = "README.md"
@@ -11,11 +11,11 @@ readme = "README.md"
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.12"
13
13
  mysql-connector-python= "^9.0.0"
14
- fluent-validation = "4.3.1"
15
14
  shapely = "^2.0.6"
16
- python-dotenv = "^1.0.1"
17
15
 
18
16
  [tool.poetry.group.test.dependencies]
17
+ fluent-validation = "4.3.1"
18
+ python-dotenv = "^1.0.1"
19
19
  pandas = "^2.2.2"
20
20
  ruff = "^0.4.5"
21
21
  parameterized = "^0.9.0"
@@ -20,3 +20,5 @@ from .model.base_model import (
20
20
  BaseModel as BaseModel,
21
21
  ORM as ORM,
22
22
  ) # COMMENT: to avoid relative import we need to import BaseModel after import Table,Column, ForeignKey, IRepositoryBase and Disassembler
23
+
24
+ from .engine import create_engine, URL # noqa: F401
@@ -23,6 +23,7 @@ class MySQLCaster(ICaster):
23
23
  NoneType: NoneTypeCaster,
24
24
  datetime: DatetimeCaster,
25
25
  bytes: BytesCaster,
26
+ bytearray: BytesCaster,
26
27
  tuple: IterableCaster,
27
28
  list: IterableCaster,
28
29
  }
@@ -12,6 +12,7 @@ from .order import Order as Order
12
12
  from .update import UpdateQuery as UpdateQuery
13
13
  from .upsert import UpsertQuery as UpsertQuery
14
14
  from .where import Where as Where
15
+ from .having import Having as Having
15
16
  from .count import Count as Count
16
17
  from .group_by import GroupBy as GroupBy
17
18
  from .alias import Alias as Alias
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+ import typing as tp
3
+
4
+ from ormlambda import Table
5
+ from ormlambda.sql.clause_info import ClauseInfo
6
+ from ormlambda.sql.clause_info.clause_info_context import ClauseContextType
7
+ from ormlambda.sql.types import TableType
8
+
9
+ if tp.TYPE_CHECKING:
10
+ from ormlambda.sql.types import ColumnType
11
+ from ormlambda import Table
12
+ from ormlambda.sql.types import AliasType
13
+
14
+
15
+ class Alias[T: Table](ClauseInfo[T]):
16
+ def __init__[TProp](
17
+ self,
18
+ table: TableType[T],
19
+ column: tp.Optional[ColumnType[TProp]] = None,
20
+ alias_table: tp.Optional[AliasType[ClauseInfo[T]]] = None,
21
+ alias_clause: tp.Optional[AliasType[ClauseInfo[T]]] = None,
22
+ context: ClauseContextType = None,
23
+ keep_asterisk: bool = False,
24
+ preserve_context: bool = False,
25
+ ):
26
+ if not alias_clause:
27
+ raise TypeError
28
+ super().__init__(table, column, alias_table, alias_clause, context, keep_asterisk, preserve_context)
@@ -0,0 +1,31 @@
1
+ import typing as tp
2
+ from ormlambda import Table
3
+ from ormlambda.sql.clause_info.clause_info import AggregateFunctionBase
4
+ from ormlambda.sql.clause_info.clause_info_context import ClauseInfoContext
5
+ from ormlambda.sql.types import ColumnType
6
+
7
+
8
+ class GroupBy[T: tp.Type[Table], *Ts, TProp](AggregateFunctionBase):
9
+ @classmethod
10
+ def FUNCTION_NAME(self) -> str:
11
+ return "GROUP BY"
12
+
13
+ def __init__(
14
+ self,
15
+ column: ColumnType,
16
+ context: ClauseInfoContext,
17
+ **kwargs,
18
+ ):
19
+ super().__init__(
20
+ table=column.table,
21
+ column=column,
22
+ alias_table=None,
23
+ alias_clause=None,
24
+ context=context,
25
+ **kwargs,
26
+ )
27
+
28
+ @property
29
+ def query(self) -> str:
30
+ column = self._create_query()
31
+ return f"{self.FUNCTION_NAME()} {column}"
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from .where import Where
4
+
5
+
6
+ class Having(Where):
7
+ """
8
+ The purpose of this class is to create 'WHERE' condition queries properly.
9
+ """
10
+
11
+ def __init__(self, *comparer, restrictive=True, context=None):
12
+ super().__init__(*comparer, restrictive=restrictive, context=context)
13
+
14
+ @staticmethod
15
+ def FUNCTION_NAME() -> str:
16
+ return "HAVING"
@@ -48,13 +48,18 @@ class Order(AggregateFunctionBase):
48
48
  columns = self.unresolved_column
49
49
 
50
50
  # if this attr is not iterable means that we only pass one column without wrapped in a list or tuple
51
+ if isinstance(columns, str):
52
+ string_columns = f"{columns} {str(self._order_type[0])}"
53
+ return f"{self.FUNCTION_NAME()} {string_columns}"
54
+
51
55
  if not isinstance(columns, tp.Iterable):
52
56
  columns = (columns,)
57
+
53
58
  assert len(columns) == len(self._order_type)
54
59
 
55
60
  context = ClauseInfoContext(table_context=self._context._table_context, clause_context=None) if self._context else None
56
61
  for index, clause in enumerate(self._convert_into_clauseInfo(columns, context)):
57
62
  clause.alias_clause = None
58
- string_columns.append(f"{clause.query} {self._order_type[index].value}")
63
+ string_columns.append(f"{clause.query} {str(self._order_type[index])}")
59
64
 
60
65
  return f"{self.FUNCTION_NAME()} {', '.join(string_columns)}"
@@ -32,8 +32,8 @@ class Where(AggregateFunctionBase):
32
32
  def alias_clause(self) -> None:
33
33
  return None
34
34
 
35
- @staticmethod
36
- def join_condition(wheres: tp.Iterable[Where], restrictive: bool, context: ClauseInfoContext) -> str:
35
+ @classmethod
36
+ def join_condition(cls, wheres: tp.Iterable[Where], restrictive: bool, context: ClauseInfoContext) -> str:
37
37
  if not isinstance(wheres, tp.Iterable):
38
38
  wheres = (wheres,)
39
39
 
@@ -42,4 +42,4 @@ class Where(AggregateFunctionBase):
42
42
  for c in where._comparer:
43
43
  c.set_context(context)
44
44
  comparers.append(c)
45
- return Where(*comparers, restrictive=restrictive, context=context).query
45
+ return cls(*comparers, restrictive=restrictive, context=context).query
@@ -1,20 +1,22 @@
1
+ import typing as tp
2
+
1
3
  from ormlambda.sql.clause_info import AggregateFunctionBase
2
4
  from ormlambda.sql.clause_info.clause_info_context import ClauseInfoContext, ClauseContextType
3
-
4
-
5
- import typing as tp
6
5
  from ormlambda.sql.types import ColumnType, AliasType
7
6
  from ormlambda.sql.clause_info import ClauseInfo
8
7
 
9
8
 
10
- class Concat[*Ts](AggregateFunctionBase):
9
+ type ConcatResponse[TProp] = tuple[str | ColumnType[TProp]]
10
+
11
+
12
+ class Concat[T](AggregateFunctionBase):
11
13
  @staticmethod
12
14
  def FUNCTION_NAME() -> str:
13
15
  return "CONCAT"
14
16
 
15
17
  def __init__[TProp](
16
18
  self,
17
- values: ColumnType[Ts] | tuple[ColumnType[Ts], ...],
19
+ values: ConcatResponse[TProp],
18
20
  alias_clause: AliasType[ColumnType[TProp]] = "concat",
19
21
  context: ClauseContextType = None,
20
22
  ) -> None:
@@ -33,6 +35,6 @@ class Concat[*Ts](AggregateFunctionBase):
33
35
  context = ClauseInfoContext(table_context=self._context._table_context, clause_context=None) if self._context else None
34
36
 
35
37
  for clause in self._convert_into_clauseInfo(self.unresolved_column, context=context):
36
- clause.alias_clause = None
38
+ clause.alias_clause = self.alias_clause
37
39
  columns.append(clause)
38
40
  return self._concat_alias_and_column(f"{self.FUNCTION_NAME()}({ClauseInfo.join_clauses(columns)})", self._alias_aggregate)
@@ -14,7 +14,7 @@ class Max(AggregateFunctionBase[None]):
14
14
 
15
15
  def __init__[TProp](
16
16
  self,
17
- elements: tuple[ColumnType[TProp], ...] | ColumnType[TProp],
17
+ elements: ColumnType[TProp],
18
18
  alias_clause: AliasType[ColumnType[TProp]] = "max",
19
19
  context: ClauseContextType = None,
20
20
  ):
@@ -27,12 +27,12 @@ class JoinContext[TParent: Table, TRepo]:
27
27
  for comparer, by in self._joins:
28
28
  fk_clause, alias = self.get_fk_clause(comparer)
29
29
 
30
- foreign_key: ForeignKey = ForeignKey(comparer=comparer, clause_name=alias)
30
+ foreign_key: ForeignKey = ForeignKey(comparer=comparer, clause_name=alias, keep_alive=True)
31
31
  fk_clause.alias_table = foreign_key.alias
32
32
  self._context.add_clause_to_context(fk_clause)
33
33
  setattr(self._parent, alias, foreign_key)
34
34
 
35
- # TODOH []: We need to preserve the 'foreign_key' variable while inside the 'with' clause.
35
+ # TODOH [x]: We need to preserve the 'foreign_key' variable while inside the 'with' clause.
36
36
  # Keep in mind that 'ForeignKey.stored_calls' is cleared every time we call methods like
37
37
  # .select(), .select_one(), .insert(), .update(), or .count(). This means we only retain
38
38
  # the context from the first call of any of these methods.
@@ -49,6 +49,7 @@ class JoinContext[TParent: Table, TRepo]:
49
49
  fk: ForeignKey = getattr(self._parent, attribute)
50
50
  delattr(self._parent, attribute)
51
51
  del self._context._table_context[fk.tright]
52
+ ForeignKey.stored_calls.remove(fk)
52
53
  return None
53
54
 
54
55
  def __getattr__(self, name: str) -> TParent:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
  import contextlib
3
3
  from pathlib import Path
4
4
  from typing import Any, Generator, Iterable, Optional, Type, override, TYPE_CHECKING, Unpack
5
+ import uuid
5
6
  import shapely as shp
6
7
 
7
8
  # from mysql.connector.pooling import MySQLConnectionPool
@@ -16,6 +17,7 @@ from ormlambda.caster import Caster
16
17
  from ..clauses import CreateDatabase, TypeExists
17
18
  from ..clauses import DropDatabase
18
19
  from ..clauses import DropTable
20
+ from ..clauses import Alias
19
21
 
20
22
 
21
23
  if TYPE_CHECKING:
@@ -68,12 +70,15 @@ class Response[TFlavour, *Ts]:
68
70
 
69
71
  def _cast_to_flavour(self, data: list[tuple[*Ts]], **kwargs) -> list[dict[str, tuple[*Ts]]] | list[tuple[*Ts]] | list[TFlavour]:
70
72
  def _dict(**kwargs) -> list[dict[str, tuple[*Ts]]]:
73
+ nonlocal data
71
74
  return [dict(zip(self._columns, x)) for x in data]
72
75
 
73
76
  def _tuple(**kwargs) -> list[tuple[*Ts]]:
77
+ nonlocal data
74
78
  return data
75
79
 
76
80
  def _set(**kwargs) -> list[set]:
81
+ nonlocal data
77
82
  for d in data:
78
83
  n = len(d)
79
84
  for i in range(n):
@@ -84,12 +89,19 @@ class Response[TFlavour, *Ts]:
84
89
  return [set(x) for x in data]
85
90
 
86
91
  def _list(**kwargs) -> list[list]:
92
+ nonlocal data
87
93
  return [list(x) for x in data]
88
94
 
89
95
  def _default(**kwargs) -> list[TFlavour]:
90
- replacer_dicc: dict[str, str] = {x.alias_clause: x.column for x in self._select.all_clauses}
96
+ nonlocal data
97
+ replacer_dicc: dict[str, str] = {}
91
98
 
92
- cleaned_column_names = [replacer_dicc[col] for col in self._columns]
99
+ for col in self._select.all_clauses:
100
+ if hasattr(col, "_alias_aggregate") or col.alias_clause is None or isinstance(col, Alias):
101
+ continue
102
+ replacer_dicc[col.alias_clause] = col.column
103
+
104
+ cleaned_column_names = [replacer_dicc.get(col, col) for col in self._columns]
93
105
 
94
106
  result = []
95
107
  for attr in data:
@@ -157,7 +169,34 @@ class MySQLRepository(BaseRepository[MySQLConnectionPool]):
157
169
  #
158
170
 
159
171
  def __init__(self, **kwargs: Unpack[MySQLArgs]):
160
- super().__init__(MySQLConnectionPool, **kwargs)
172
+ timeout = self.__add_connection_timeout(kwargs)
173
+ name = self.__add_pool_name(kwargs)
174
+ size = self.__add_pool_size(kwargs)
175
+ attr = kwargs.copy()
176
+ attr["connection_timeout"] = timeout
177
+ attr["pool_name"] = name
178
+ attr["pool_size"] = size
179
+
180
+ super().__init__(MySQLConnectionPool, **attr)
181
+
182
+ @staticmethod
183
+ def __add_connection_timeout(kwargs: MySQLArgs) -> int:
184
+ if "connection_timeout" not in kwargs.keys():
185
+ return 60
186
+ return int(kwargs.pop("connection_timeout"))
187
+
188
+ @staticmethod
189
+ def __add_pool_name(kwargs: MySQLArgs) -> str:
190
+ if "pool_name" not in kwargs.keys():
191
+ return str(uuid.uuid4())
192
+
193
+ return kwargs.pop("pool_name")
194
+
195
+ @staticmethod
196
+ def __add_pool_size(kwargs: MySQLArgs) -> int:
197
+ if "pool_size" not in kwargs.keys():
198
+ return 5
199
+ return int(kwargs.pop("pool_size"))
161
200
 
162
201
  @contextlib.contextmanager
163
202
  def get_connection(self) -> Generator[MySQLConnection, None, None]:
@@ -254,7 +293,6 @@ class MySQLRepository(BaseRepository[MySQLConnectionPool]):
254
293
 
255
294
  @override
256
295
  def database_exists(self, name: str) -> bool:
257
- query = "SHOW DATABASES LIKE %s;"
258
296
  temp_config = self._pool._cnx_config
259
297
 
260
298
  config_without_db = temp_config.copy()
@@ -262,10 +300,11 @@ class MySQLRepository(BaseRepository[MySQLConnectionPool]):
262
300
  if "database" in config_without_db:
263
301
  config_without_db.pop("database")
264
302
  self._pool.set_config(**config_without_db)
303
+
265
304
  with self.get_connection() as cnx:
266
305
  with cnx.cursor(buffered=True) as cursor:
267
- cursor.execute(query, (name,))
268
- res = cursor.fetchmany(1)
306
+ cursor.execute("SHOW DATABASES LIKE %s;", (name,))
307
+ res = cursor.fetchmany(1)
269
308
 
270
309
  self._pool.set_config(**temp_config)
271
310
  return len(res) > 0
@@ -276,12 +315,11 @@ class MySQLRepository(BaseRepository[MySQLConnectionPool]):
276
315
 
277
316
  @override
278
317
  def table_exists(self, name: str) -> bool:
279
- query = "SHOW TABLES LIKE %s;"
280
318
  with self.get_connection() as cnx:
281
319
  if not cnx.database:
282
320
  raise Exception("No database selected")
283
321
  with cnx.cursor(buffered=True) as cursor:
284
- cursor.execute(query, (name,))
322
+ cursor.execute("SHOW TABLES LIKE %s;", (name,))
285
323
  res = cursor.fetchmany(1)
286
324
  return len(res) > 0
287
325
 
@@ -297,9 +335,17 @@ class MySQLRepository(BaseRepository[MySQLConnectionPool]):
297
335
 
298
336
  @property
299
337
  def database(self) -> Optional[str]:
300
- return self._data_config.get("database", None)
338
+ return self._pool._cnx_config.get("database", None)
301
339
 
302
340
  @database.setter
303
341
  def database(self, value: str) -> None:
304
- self._data_config["database"] = value
305
- self._pool.set_config(**self._data_config)
342
+ """Change the current database using USE statement"""
343
+
344
+ if not self.database_exists(value):
345
+ raise ValueError(f"You cannot set the non-existent '{value}' database.")
346
+
347
+ old_config: MySQLArgs = self._pool._cnx_config.copy()
348
+ old_config["database"] = value
349
+
350
+ self._pool._remove_connections()
351
+ self._pool = type(self)(**old_config)._pool