future-framework 1.1.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.
Files changed (64) hide show
  1. future/__init__.py +3 -0
  2. future/application.py +656 -0
  3. future/authentication/Auth0Authentication.py +9 -0
  4. future/authentication/Authentication.py +11 -0
  5. future/authentication/AzureADAuthentication.py +9 -0
  6. future/authentication/BasicAuthentication.py +9 -0
  7. future/authentication/KerberosAuthentication.py +9 -0
  8. future/authentication/KeycloakAuthentication.py +9 -0
  9. future/authentication/OAuth2Authentication.py +9 -0
  10. future/authentication/OpenIdConnectAuthentication.py +9 -0
  11. future/authentication/SAMLAuthentication.py +9 -0
  12. future/cli/__init__.py +1 -0
  13. future/cli/main.py +608 -0
  14. future/cli/stubs.py +114 -0
  15. future/controllers/__init__.py +4 -0
  16. future/controllers/base.py +8 -0
  17. future/controllers/builtins.py +41 -0
  18. future/controllers/graphql.py +23 -0
  19. future/controllers/openapi.py +224 -0
  20. future/databases/Clickhouse.py +176 -0
  21. future/databases/Connections.py +20 -0
  22. future/databases/Database.py +66 -0
  23. future/databases/Elasticsearch.py +146 -0
  24. future/databases/MongoDB.py +177 -0
  25. future/databases/MySQL.py +215 -0
  26. future/databases/Postgres.py +214 -0
  27. future/databases/Redis.py +146 -0
  28. future/databases/SQLite.py +219 -0
  29. future/exceptions.py +27 -0
  30. future/graphql/__init__.py +1 -0
  31. future/graphql/schema.py +148 -0
  32. future/lifespan.py +70 -0
  33. future/logger.py +53 -0
  34. future/middleware/Middleware.py +201 -0
  35. future/middleware/SessionMiddleware.py +77 -0
  36. future/middleware/__init__.py +21 -0
  37. future/migrations/Blueprint.py +54 -0
  38. future/migrations/Column.py +33 -0
  39. future/migrations/Migration.py +11 -0
  40. future/migrations/MigrationGenerator.py +137 -0
  41. future/migrations/Migrator.py +72 -0
  42. future/migrations/Schema.py +21 -0
  43. future/models/__init__.py +1 -0
  44. future/models/model.py +224 -0
  45. future/openapi.py +233 -0
  46. future/plugins/ElasticsearchPlugin.py +496 -0
  47. future/plugins/__init__.py +9 -0
  48. future/request.py +143 -0
  49. future/response.py +201 -0
  50. future/routing.py +342 -0
  51. future/seeds/SeedGenerator.py +95 -0
  52. future/seeds/SeedRunner.py +42 -0
  53. future/seeds/Seeder.py +9 -0
  54. future/settings.py +94 -0
  55. future/tasks/__init__.py +14 -0
  56. future/tasks/scheduler.py +267 -0
  57. future/testing/__init__.py +1 -0
  58. future/testing/client.py +135 -0
  59. future/types.py +47 -0
  60. future_framework-1.1.0.dist-info/METADATA +68 -0
  61. future_framework-1.1.0.dist-info/RECORD +64 -0
  62. future_framework-1.1.0.dist-info/WHEEL +4 -0
  63. future_framework-1.1.0.dist-info/entry_points.txt +3 -0
  64. future_framework-1.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,137 @@
1
+ # Generate Schema migrations from Model annotations.
2
+
3
+
4
+ import importlib.util
5
+ import inflection
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from types import UnionType
9
+ from typing import get_args, get_origin, Union
10
+
11
+ from future.models import Model
12
+
13
+
14
+ class MigrationGenerator:
15
+ def __init__(self, models_path="app/models", migrations_path="database/migrations"):
16
+ self.models_path = models_path
17
+ self.migrations_path = migrations_path
18
+
19
+ def find_model(self, name):
20
+ for model in self.discover_models():
21
+ if model.__name__ == name:
22
+ return model
23
+ raise ValueError(f"Model not found: {name}")
24
+
25
+ def discover_models(self):
26
+ directory = Path(self.models_path)
27
+ if not directory.exists():
28
+ raise FileNotFoundError(f"Models path not found: {self.models_path}")
29
+ models = []
30
+ seen = set()
31
+ for file_path in sorted(directory.glob("*.py")):
32
+ if file_path.name.startswith("_"):
33
+ continue
34
+ module_name = f"app_model_{file_path.stem}"
35
+ spec = importlib.util.spec_from_file_location(module_name, str(file_path))
36
+ module = importlib.util.module_from_spec(spec)
37
+ spec.loader.exec_module(module)
38
+ for value in module.__dict__.values():
39
+ if not isinstance(value, type) or not issubclass(value, Model) or value is Model:
40
+ continue
41
+ if value.__name__ in seen:
42
+ continue
43
+ if not getattr(value, "__annotations__", None):
44
+ continue
45
+ seen.add(value.__name__)
46
+ models.append(value)
47
+ return models
48
+
49
+ def table_name(self, model):
50
+ return model.__table__ or inflection.tableize(model.__name__)
51
+
52
+ def column_line(self, field, annotation):
53
+ nullable = False
54
+ origin = get_origin(annotation)
55
+ if origin is Union or origin is UnionType:
56
+ args = get_args(annotation)
57
+ nullable = type(None) in args
58
+ non_null = [arg for arg in args if arg is not type(None)]
59
+ annotation = non_null[0] if non_null else str
60
+ if field == "id":
61
+ return "table.id()"
62
+ type_name = getattr(annotation, "__name__", str(annotation))
63
+ if annotation is datetime or type_name == "datetime":
64
+ line = f'table.datetime("{field}")'
65
+ elif annotation is int or type_name == "int":
66
+ line = f'table.integer("{field}")'
67
+ elif annotation is float or type_name == "float":
68
+ line = f'table.float("{field}")'
69
+ elif annotation is bool or type_name == "bool":
70
+ line = f'table.boolean("{field}")'
71
+ elif field in ("description", "text", "body", "content") or field.endswith("_original") or field.endswith("_translated"):
72
+ line = f'table.text("{field}")'
73
+ else:
74
+ line = f'table.string("{field}")'
75
+ if nullable:
76
+ line += ".nullable()"
77
+ return line
78
+
79
+ def blueprint_lines(self, model):
80
+ annotations = dict(getattr(model, "__annotations__", {}))
81
+ lines = []
82
+ if "created_at" in annotations and "updated_at" in annotations:
83
+ annotations.pop("created_at")
84
+ annotations.pop("updated_at")
85
+ use_timestamps = True
86
+ else:
87
+ use_timestamps = False
88
+ for field, annotation in annotations.items():
89
+ lines.append(f" {self.column_line(field, annotation)}")
90
+ if use_timestamps:
91
+ lines.append(" table.timestamps()")
92
+ return lines
93
+
94
+ def render(self, model):
95
+ table = self.table_name(model)
96
+ class_name = f"Create{inflection.camelize(table)}"
97
+ connection = getattr(model, "__connection__", "default")
98
+ columns = "\n".join(self.blueprint_lines(model))
99
+ return (
100
+ "from future.migrations.Migration import Migration\n"
101
+ "from future.migrations.Schema import Schema\n"
102
+ "\n"
103
+ "\n"
104
+ f"class {class_name}(Migration):\n"
105
+ f' __connection__ = "{connection}"\n'
106
+ "\n"
107
+ " def up(self):\n"
108
+ f' with Schema.create("{table}") as table:\n'
109
+ f"{columns}\n"
110
+ "\n"
111
+ " def down(self):\n"
112
+ f' Schema.drop("{table}")\n'
113
+ )
114
+
115
+ def make(self, model_name=None):
116
+ if model_name is None:
117
+ return self.make_all()
118
+ model = self.find_model(model_name)
119
+ return [self.write(model)]
120
+
121
+ def make_all(self):
122
+ paths = []
123
+ for index, model in enumerate(self.discover_models()):
124
+ paths.append(self.write(model, stamp_offset=index))
125
+ return paths
126
+
127
+ def write(self, model, stamp_offset=0):
128
+ table = self.table_name(model)
129
+ stamp = datetime.now().strftime("%Y_%m_%d_%H%M%S")
130
+ if stamp_offset:
131
+ stamp = f"{stamp}_{stamp_offset:02d}"
132
+ file_name = f"{stamp}_create_{table}.py"
133
+ directory = Path(self.migrations_path)
134
+ directory.mkdir(parents=True, exist_ok=True)
135
+ path = directory / file_name
136
+ path.write_text(self.render(model))
137
+ return str(path)
@@ -0,0 +1,72 @@
1
+ # Discovers and runs migration files against Connections.
2
+
3
+
4
+ import importlib.util
5
+ from pathlib import Path
6
+
7
+ from future.databases.Connections import Connections
8
+ from .Migration import Migration
9
+ from .Schema import Schema
10
+
11
+
12
+ class Migrator:
13
+ def __init__(self, path):
14
+ self.path = path
15
+
16
+ def discover(self):
17
+ migrations = []
18
+ directory = Path(self.path)
19
+ if not directory.exists():
20
+ return migrations
21
+ for file_path in sorted(directory.glob("*.py")):
22
+ if file_path.name.startswith("_"):
23
+ continue
24
+ module_name = f"migration_{file_path.stem}"
25
+ spec = importlib.util.spec_from_file_location(module_name, str(file_path))
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ for value in module.__dict__.values():
29
+ if isinstance(value, type) and issubclass(value, Migration) and value is not Migration:
30
+ migrations.append((file_path.stem, value))
31
+ break
32
+ return migrations
33
+
34
+ def run(self):
35
+ applied_by_connection = {}
36
+ batch_by_connection = {}
37
+ ran = []
38
+ for name, migration_class in self.discover():
39
+ migration = migration_class()
40
+ connection_name = getattr(migration, "__connection__", "default")
41
+ connection = Connections().get_connection(connection_name)
42
+ if connection_name not in applied_by_connection:
43
+ applied_by_connection[connection_name] = set(connection.migrations_get())
44
+ batch_by_connection[connection_name] = connection.migrations_max_batch() + 1
45
+ if name in applied_by_connection[connection_name]:
46
+ continue
47
+ Schema.connection(connection_name)
48
+ migration.up()
49
+ connection.migrations_put(name, batch_by_connection[connection_name])
50
+ ran.append(name)
51
+ return ran
52
+
53
+ def rollback(self):
54
+ rolled = []
55
+ by_connection = {}
56
+ for name, migration_class in self.discover():
57
+ migration = migration_class()
58
+ connection_name = getattr(migration, "__connection__", "default")
59
+ by_connection.setdefault(connection_name, []).append((name, migration))
60
+ for connection_name, items in by_connection.items():
61
+ connection = Connections().get_connection(connection_name)
62
+ max_batch = connection.migrations_max_batch()
63
+ if max_batch == 0:
64
+ continue
65
+ for name, migration in reversed(items):
66
+ if connection.migrations_batch_for(name) != max_batch:
67
+ continue
68
+ Schema.connection(connection_name)
69
+ migration.down()
70
+ connection.migrations_delete(name)
71
+ rolled.append(name)
72
+ return rolled
@@ -0,0 +1,21 @@
1
+ # Orator/Masonite-style schema facade. Resolves connections via Connections.
2
+
3
+
4
+ from .Blueprint import Blueprint
5
+ from future.databases.Connections import Connections
6
+
7
+
8
+ class SchemaMeta(type):
9
+ def connection(cls, name):
10
+ cls._connection_name = name
11
+ return cls
12
+
13
+ def create(cls, name):
14
+ return Blueprint(name, cls._connection_name, action="create")
15
+
16
+ def drop(cls, name):
17
+ Connections().get_connection(cls._connection_name).schema_drop(name)
18
+
19
+
20
+ class Schema(metaclass=SchemaMeta):
21
+ _connection_name = "default"
@@ -0,0 +1 @@
1
+ from future.models.model import Model, Query
future/models/model.py ADDED
@@ -0,0 +1,224 @@
1
+ from future.databases.Connections import Connections
2
+ from future.logger import log
3
+ import inflection
4
+ import json
5
+
6
+ # Active-record base model (Masonite-style).
7
+ # Trade.find("1"), Trade.where("user_id", "123").get(), trade.save()
8
+
9
+
10
+ class ModelMeta(type):
11
+ def __getattr__(self, attribute, *args, **kwargs):
12
+ instantiated = self()
13
+ return getattr(instantiated, attribute)
14
+
15
+
16
+ class Query:
17
+ def __init__(self, model):
18
+ self.model = model
19
+ self.wheres = []
20
+ self.orders = []
21
+ self.limit_value = None
22
+
23
+ def where(self, column, *args):
24
+ if len(args) == 1:
25
+ operator, value = "=", args[0]
26
+ else:
27
+ operator, value = args[0], args[1]
28
+ self.wheres.append((column, operator, value))
29
+ return self
30
+
31
+ def order_by(self, column, direction="asc"):
32
+ self.orders.append((column, direction))
33
+ return self
34
+
35
+ def limit(self, value):
36
+ self.limit_value = value
37
+ return self
38
+
39
+ def get(self):
40
+ return self.model.connection().get(self.model, self.wheres, limit=self.limit_value, orders=self.orders)
41
+
42
+ def first(self):
43
+ results = self.model.connection().get(self.model, self.wheres, limit=1, orders=self.orders)
44
+ return results[0] if results else None
45
+
46
+ def __repr__(self):
47
+ return "<Query; call .get() or .first() to execute>"
48
+
49
+ def __len__(self):
50
+ raise TypeError("Query has no len(); call .get() or .first() first")
51
+
52
+ def __iter__(self):
53
+ raise TypeError("Query is not iterable; call .get() or .first() first")
54
+
55
+
56
+ class Model(metaclass=ModelMeta):
57
+ __table__ = None
58
+ __connection__ = "default"
59
+
60
+ def __init__(self, **attributes):
61
+ for key, value in attributes.items():
62
+ setattr(self, key, value)
63
+
64
+ # Why do we use __getattr__ for find and all, but not for save, delete, update?
65
+ # ModelMeta.__getattr__ turns Trade.find into “make an empty instance, then get find from it.” The instance __getattr__ then returns the callable. That lets class-level calls work without @classmethod (which this codebase forbids).
66
+ def __getattr__(self, attribute):
67
+ if attribute == "find":
68
+ def find(id):
69
+ return self.connection().find(self, id)
70
+ return find
71
+ if attribute == "all":
72
+ def all():
73
+ return self.connection().all(self)
74
+ return all
75
+ if attribute == "where":
76
+ def where(*args):
77
+ return Query(self).where(*args)
78
+ return where
79
+ if attribute == "order_by":
80
+ def order_by(*args):
81
+ return Query(self).order_by(*args)
82
+ return order_by
83
+ if attribute == "transaction":
84
+ def transaction():
85
+ return self.connection().transaction()
86
+ return transaction
87
+ if attribute == "eager_load":
88
+ def eager_load(models, name, related, foreign_key=None, kind="belongs_to"):
89
+ if not models:
90
+ return models
91
+ if kind == "belongs_to":
92
+ key = foreign_key or (inflection.underscore(related.__name__) + "_id")
93
+ ids = list({getattr(model, key, None) for model in models if getattr(model, key, None) is not None})
94
+ related_map = {}
95
+ if ids:
96
+ for row in related.where("id", "in", ids).get():
97
+ related_map[getattr(row, "id")] = row
98
+ for model in models:
99
+ setattr(model, name, related_map.get(getattr(model, key, None)))
100
+ return models
101
+ key = foreign_key or (inflection.underscore(models[0].__class__.__name__) + "_id")
102
+ parent_ids = [getattr(model, "id") for model in models if getattr(model, "id", None) is not None]
103
+ children = related.where(key, "in", parent_ids).get() if parent_ids else []
104
+ grouped = {}
105
+ for child in children:
106
+ grouped.setdefault(getattr(child, key), []).append(child)
107
+ for model in models:
108
+ setattr(model, name, grouped.get(getattr(model, "id"), []))
109
+ return models
110
+ return eager_load
111
+ raise AttributeError(attribute)
112
+
113
+ def tableize(self):
114
+ return self.__table__ or inflection.tableize(self.__class__.__name__)
115
+
116
+ def connection(self):
117
+ name = self.__connection__
118
+ if name == "default":
119
+ name = Connections._default
120
+ connection = Connections().get_connection(self.__connection__)
121
+ log.debug("%s using database connection %s (%s)", self.__class__.__name__, name, type(connection).__name__)
122
+ table_exists = getattr(connection, "table_exists", None)
123
+ if table_exists is not None:
124
+ table = self.tableize()
125
+ if not table_exists(table):
126
+ message = f'Table "{table}" does not exist on connection {name}. Run: future migrate'
127
+ log.warning(message)
128
+ raise RuntimeError(message)
129
+ return connection
130
+
131
+ def to_dict(self):
132
+ return dict(self.__dict__)
133
+
134
+ def to_json(self):
135
+ return json.dumps(self.to_dict())
136
+
137
+ def openapi_schema(self):
138
+ from datetime import date, datetime
139
+ from typing import get_args, get_origin, Union
140
+ import types
141
+ properties = {}
142
+ required = []
143
+ annotations = {}
144
+ for cls in reversed(type(self).__mro__):
145
+ if cls is object:
146
+ continue
147
+ annotations.update(getattr(cls, "__annotations__", {}))
148
+ for name, annotation in annotations.items():
149
+ if name.startswith("_"):
150
+ continue
151
+ optional = False
152
+ current = annotation
153
+ origin = get_origin(current)
154
+ args = get_args(current)
155
+ if origin is Union or isinstance(current, types.UnionType) or origin is types.UnionType:
156
+ non_none = [item for item in args if item is not type(None)]
157
+ if type(None) in args and len(non_none) == 1:
158
+ optional = True
159
+ current = non_none[0]
160
+ origin = get_origin(current)
161
+ args = get_args(current)
162
+ if origin is list:
163
+ item = args[0] if args else str
164
+ if isinstance(item, type) and issubclass(item, Model):
165
+ item_schema = {"$ref": f"#/components/schemas/{item.__name__}"}
166
+ elif item is int:
167
+ item_schema = {"type": "integer"}
168
+ elif item is float:
169
+ item_schema = {"type": "number"}
170
+ elif item is bool:
171
+ item_schema = {"type": "boolean"}
172
+ elif item in (datetime, date):
173
+ item_schema = {"type": "string", "format": "date-time" if item is datetime else "date"}
174
+ else:
175
+ item_schema = {"type": "string"}
176
+ properties[name] = {"type": "array", "items": item_schema}
177
+ elif isinstance(current, type) and issubclass(current, Model):
178
+ properties[name] = {"$ref": f"#/components/schemas/{current.__name__}"}
179
+ elif current is int:
180
+ properties[name] = {"type": "integer"}
181
+ elif current is float:
182
+ properties[name] = {"type": "number"}
183
+ elif current is bool:
184
+ properties[name] = {"type": "boolean"}
185
+ elif current is datetime:
186
+ properties[name] = {"type": "string", "format": "date-time"}
187
+ elif current is date:
188
+ properties[name] = {"type": "string", "format": "date"}
189
+ elif current is dict:
190
+ properties[name] = {"type": "object"}
191
+ else:
192
+ properties[name] = {"type": "string"}
193
+ if not optional:
194
+ required.append(name)
195
+ schema = {"type": "object", "properties": properties}
196
+ if required:
197
+ schema["required"] = required
198
+ return schema
199
+
200
+ def __repr__(self):
201
+ return str(self.to_dict())
202
+
203
+ def save(self):
204
+ return self.connection().save(self)
205
+
206
+ def delete(self):
207
+ return self.connection().delete(self)
208
+
209
+ def update(self, **changes):
210
+ for key, value in changes.items():
211
+ setattr(self, key, value)
212
+ self.connection().update(self, changes)
213
+ return self
214
+
215
+ def belongs_to(self, related, foreign_key=None):
216
+ key = foreign_key or (inflection.underscore(related.__name__) + "_id")
217
+ related_id = getattr(self, key, None)
218
+ if related_id is None:
219
+ return None
220
+ return related.find(related_id)
221
+
222
+ def has_many(self, related, foreign_key=None):
223
+ key = foreign_key or (inflection.underscore(self.__class__.__name__) + "_id")
224
+ return related.where(key, getattr(self, "id")).get()