kapsikkum-unmanic – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | #!/usr/bin/env python3 |
2 | # -*- coding: utf-8 -*- |
||
3 | |||
4 | """ |
||
5 | unmanic.database.py |
||
6 | |||
7 | Written by: Josh.5 <jsunnex@gmail.com> |
||
8 | Date: 14 Aug 2021, (12:03 PM) |
||
9 | |||
10 | Copyright: |
||
11 | Copyright (C) Josh Sunnex - All Rights Reserved |
||
12 | |||
13 | Permission is hereby granted, free of charge, to any person obtaining a copy |
||
14 | of this software and associated documentation files (the "Software"), to deal |
||
15 | in the Software without restriction, including without limitation the rights |
||
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
17 | copies of the Software, and to permit persons to whom the Software is |
||
18 | furnished to do so, subject to the following conditions: |
||
19 | |||
20 | The above copyright notice and this permission notice shall be included in all |
||
21 | copies or substantial portions of the Software. |
||
22 | |||
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
||
26 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
||
27 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
||
28 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE |
||
29 | OR OTHER DEALINGS IN THE SOFTWARE. |
||
30 | |||
31 | """ |
||
32 | import importlib |
||
33 | import inspect |
||
34 | import os |
||
35 | import sys |
||
36 | |||
37 | from peewee import Model, SqliteDatabase, Field |
||
38 | from peewee_migrate import Migrator, Router |
||
39 | |||
40 | from unmanic.libs import unlogger |
||
41 | from unmanic.libs.unmodels.lib import BaseModel |
||
42 | |||
43 | |||
44 | class Migrations(object): |
||
45 | """ |
||
46 | Migrations |
||
47 | |||
48 | Handle all migrations during application start. |
||
49 | """ |
||
50 | |||
51 | logger = None |
||
52 | database = None |
||
53 | |||
54 | def __init__(self, config): |
||
55 | unmanic_logging = unlogger.UnmanicLogger.__call__() |
||
56 | self.logger = unmanic_logging.get_logger(__class__.__name__) |
||
57 | |||
58 | # Based on configuration, select database to connect to. |
||
59 | if config['TYPE'] == 'SQLITE': |
||
60 | # Create SQLite directory if not exists |
||
61 | db_file_directory = os.path.dirname(config['FILE']) |
||
62 | if not os.path.exists(db_file_directory): |
||
63 | os.makedirs(db_file_directory) |
||
64 | self.database = SqliteDatabase(config['FILE']) |
||
65 | |||
66 | self.router = Router(database=self.database, |
||
67 | migrate_table='migratehistory_{}'.format(config.get('MIGRATIONS_HISTORY_VERSION')), |
||
68 | migrate_dir=config.get('MIGRATIONS_DIR'), |
||
69 | logger=self.logger) |
||
70 | |||
71 | self.migrator = Migrator(self.database) |
||
72 | |||
73 | def __log(self, message, level='info'): |
||
74 | if self.logger: |
||
75 | getattr(self.logger, level)(message) |
||
76 | else: |
||
77 | print(message) |
||
78 | |||
79 | def __run_all_migrations(self): |
||
80 | """ |
||
81 | Run all new migrations. |
||
82 | Migrations that have already been run will be ignored. |
||
83 | |||
84 | :return: |
||
85 | """ |
||
86 | self.router.run() |
||
87 | |||
88 | def update_schema(self): |
||
89 | """ |
||
90 | Updates the Unmanic database schema. |
||
91 | |||
92 | Newly added tables/models and columns/fields will be automatically generated by this function. |
||
93 | This way we do not need to create a migration script unless we: |
||
94 | - rename a column/field |
||
95 | - delete a column/field |
||
96 | - delete a table/model |
||
97 | |||
98 | :return: |
||
99 | """ |
||
100 | # Fetch all model classes |
||
101 | discovered_models = inspect.getmembers(sys.modules["unmanic.libs.unmodels"], inspect.isclass) |
||
102 | all_models = [tup[1] for tup in discovered_models] |
||
103 | |||
104 | # Start by creating all models |
||
105 | self.__log("Initialising database tables") |
||
106 | try: |
||
107 | with self.database.transaction(): |
||
108 | for model in all_models: |
||
109 | self.migrator.create_table(model) |
||
110 | self.migrator.run() |
||
111 | except Exception: |
||
112 | self.database.rollback() |
||
113 | self.__log("Initialising tables failed", level='exception') |
||
114 | raise |
||
115 | |||
116 | # Migrations will only be used for removing obsolete columns |
||
117 | self.__run_all_migrations() |
||
118 | |||
119 | # Newly added fields can be auto added with this function... no need for a migration script |
||
120 | # Ensure all files are also present for each of the model classes |
||
121 | self.__log("Updating database fields") |
||
122 | for model in all_models: |
||
123 | if issubclass(model, BaseModel): |
||
124 | # Fetch all peewee fields for the model class |
||
125 | # https://stackoverflow.com/questions/22573558/peewee-determining-meta-data-about-model-at-run-time |
||
126 | fields = model._meta.fields |
||
127 | # loop over the fields and ensure each on exists in the table |
||
128 | field_keys = [f for f in fields] |
||
129 | for fk in field_keys: |
||
130 | field = fields.get(fk) |
||
131 | if isinstance(field, Field): |
||
132 | if not any(f for f in self.database.get_columns(model._meta.name) if f.name == field.name): |
||
133 | # Field does not exist in DB table |
||
134 | self.__log("Adding missing column") |
||
135 | try: |
||
136 | with self.database.transaction(): |
||
137 | self.migrator.add_columns(model, **{field.name: field}) |
||
138 | self.migrator.run() |
||
139 | except Exception: |
||
140 | self.database.rollback() |
||
141 | self.__log("Update failed", level='exception') |
||
142 | raise |