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.
- future/__init__.py +3 -0
- future/application.py +656 -0
- future/authentication/Auth0Authentication.py +9 -0
- future/authentication/Authentication.py +11 -0
- future/authentication/AzureADAuthentication.py +9 -0
- future/authentication/BasicAuthentication.py +9 -0
- future/authentication/KerberosAuthentication.py +9 -0
- future/authentication/KeycloakAuthentication.py +9 -0
- future/authentication/OAuth2Authentication.py +9 -0
- future/authentication/OpenIdConnectAuthentication.py +9 -0
- future/authentication/SAMLAuthentication.py +9 -0
- future/cli/__init__.py +1 -0
- future/cli/main.py +608 -0
- future/cli/stubs.py +114 -0
- future/controllers/__init__.py +4 -0
- future/controllers/base.py +8 -0
- future/controllers/builtins.py +41 -0
- future/controllers/graphql.py +23 -0
- future/controllers/openapi.py +224 -0
- future/databases/Clickhouse.py +176 -0
- future/databases/Connections.py +20 -0
- future/databases/Database.py +66 -0
- future/databases/Elasticsearch.py +146 -0
- future/databases/MongoDB.py +177 -0
- future/databases/MySQL.py +215 -0
- future/databases/Postgres.py +214 -0
- future/databases/Redis.py +146 -0
- future/databases/SQLite.py +219 -0
- future/exceptions.py +27 -0
- future/graphql/__init__.py +1 -0
- future/graphql/schema.py +148 -0
- future/lifespan.py +70 -0
- future/logger.py +53 -0
- future/middleware/Middleware.py +201 -0
- future/middleware/SessionMiddleware.py +77 -0
- future/middleware/__init__.py +21 -0
- future/migrations/Blueprint.py +54 -0
- future/migrations/Column.py +33 -0
- future/migrations/Migration.py +11 -0
- future/migrations/MigrationGenerator.py +137 -0
- future/migrations/Migrator.py +72 -0
- future/migrations/Schema.py +21 -0
- future/models/__init__.py +1 -0
- future/models/model.py +224 -0
- future/openapi.py +233 -0
- future/plugins/ElasticsearchPlugin.py +496 -0
- future/plugins/__init__.py +9 -0
- future/request.py +143 -0
- future/response.py +201 -0
- future/routing.py +342 -0
- future/seeds/SeedGenerator.py +95 -0
- future/seeds/SeedRunner.py +42 -0
- future/seeds/Seeder.py +9 -0
- future/settings.py +94 -0
- future/tasks/__init__.py +14 -0
- future/tasks/scheduler.py +267 -0
- future/testing/__init__.py +1 -0
- future/testing/client.py +135 -0
- future/types.py +47 -0
- future_framework-1.1.0.dist-info/METADATA +68 -0
- future_framework-1.1.0.dist-info/RECORD +64 -0
- future_framework-1.1.0.dist-info/WHEEL +4 -0
- future_framework-1.1.0.dist-info/entry_points.txt +3 -0
- 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()
|