Skip to content

Commit 587de56

Browse files
committed
Modified: form config on API call, not at runtime (#37)
1 parent ef899bc commit 587de56

File tree

3 files changed

+90
-27
lines changed

3 files changed

+90
-27
lines changed

libreforms_fastapi/app/__init__.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@
6767
)
6868

6969
from libreforms_fastapi.utils.pydantic_models import (
70-
example_form_config,
71-
generate_html_form,
72-
generate_pydantic_models,
70+
# example_form_config,
71+
# generate_html_form,
72+
# generate_pydantic_models,
73+
get_form_config,
74+
get_form_names,
7375
CreateUserRequest,
7476
)
7577

@@ -207,16 +209,12 @@ async def start_test_logger():
207209
logger.info('Relational database has been initialized')
208210

209211

210-
# Yield the pydantic form model
211-
form_config = example_form_config
212-
FormModels = generate_pydantic_models(form_config)
213-
214212
# Initialize the document database
215213
if config.MONGODB_ENABLED:
216-
DocumentDatabase = ManageMongoDB(config=form_config, timezone=config.TIMEZONE, env=config.ENVIRONMENT)
214+
DocumentDatabase = ManageMongoDB(form_names_callable=get_form_names, timezone=config.TIMEZONE, env=config.ENVIRONMENT)
217215
logger.info('MongoDB has been initialized')
218216
else:
219-
DocumentDatabase = ManageTinyDB(config=form_config, timezone=config.TIMEZONE, env=config.ENVIRONMENT)
217+
DocumentDatabase = ManageTinyDB(form_names_callable=get_form_names, timezone=config.TIMEZONE, env=config.ENVIRONMENT)
220218
logger.info('TinyDB has been initialized')
221219

222220
# Here we define an API key header for the api view functions.
@@ -359,11 +357,11 @@ async def verify_key_details(key: str = Depends(X_API_KEY)):
359357
@app.post("/api/form/create/{form_name}", dependencies=[Depends(api_key_auth)])
360358
async def api_form_create(form_name: str, background_tasks: BackgroundTasks, request: Request, session: SessionLocal = Depends(get_db), key: str = Depends(X_API_KEY), body: Dict = Body(...)):
361359

362-
if form_name not in form_config:
360+
if form_name not in get_form_names():
363361
raise HTTPException(status_code=404, detail=f"Form '{form_name}' not found")
364362

365-
# Pull this form model from the list of available models
366-
FormModel = FormModels[form_name]
363+
# Yield the pydantic form model
364+
FormModel = get_form_config(form_name=form_name)
367365

368366
# # Here we validate and coerce data into its proper type
369367
form_data = FormModel.model_validate(body)

libreforms_fastapi/utils/document_database.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ def __init__(self, form_name):
144144
super().__init__(message)
145145

146146
class ManageDocumentDB(ABC):
147-
def __init__(self, config: dict, timezone: ZoneInfo):
148-
self.config = config
147+
def __init__(self, form_names_callable, timezone: ZoneInfo):
148+
self.form_names_callable = form_names_callable
149149

150150
# Set default log_name if not already set by a subclass
151151
if not hasattr(self, 'log_name'):
@@ -176,6 +176,11 @@ def _initialize_database_collections(self):
176176
"""Establishes database instances / collections for each form."""
177177
pass
178178

179+
# @abstractmethod
180+
# def _update_database_collections(self, form_names_callable):
181+
# """Idempotent method to update available collections."""
182+
# pass
183+
179184
@abstractmethod
180185
def _check_form_exists(self, form_name:str):
181186
"""Checks if the form exists in the configuration."""
@@ -238,7 +243,7 @@ def restore_database_from_backup(self, form_name:str, backup_filename:str, backu
238243

239244

240245
class ManageTinyDB(ManageDocumentDB):
241-
def __init__(self, config: dict, timezone: ZoneInfo, db_path: str = "instance/", use_logger=True, env="development"):
246+
def __init__(self, form_names_callable, timezone: ZoneInfo, db_path: str = "instance/", use_logger=True, env="development"):
242247
self.db_path = db_path
243248
os.makedirs(self.db_path, exist_ok=True)
244249

@@ -252,7 +257,7 @@ def __init__(self, config: dict, timezone: ZoneInfo, db_path: str = "instance/",
252257
namespace=self.log_name
253258
)
254259

255-
super().__init__(config, timezone)
260+
super().__init__(form_names_callable, timezone)
256261

257262
# Here we create a Query object to ship with the class
258263
self.Form = Query()
@@ -262,7 +267,7 @@ def _initialize_database_collections(self):
262267
"""Establishes database instances for each form."""
263268
# Initialize databases
264269
self.databases = {}
265-
for form_name in self.config.keys():
270+
for form_name in self.form_names_callable():
266271
# self.databases[form_name] = TinyDB(self._get_db_path(form_name))
267272
self.databases[form_name] = CustomTinyDB(self._get_db_path(form_name))
268273

@@ -272,9 +277,15 @@ def _get_db_path(self, form_name:str):
272277

273278
def _check_form_exists(self, form_name:str):
274279
"""Checks if the form exists in the configuration."""
275-
if form_name not in self.config:
280+
if form_name not in self.form_names_callable():
276281
raise CollectionDoesNotExist(form_name)
277282

283+
# If a form name is found in the callable but not in the collections, reinitialize.
284+
# This probably means there has been a change to the form config. This class should
285+
# be able to work even when configuration data changes.
286+
if form_name not in self.databases.keys():
287+
self._initialize_database_collections()
288+
278289
def create_document(self, form_name:str, json_data, metadata={}):
279290
"""Adds json data to the specified form's database."""
280291
self._check_form_exists(form_name)

libreforms_fastapi/utils/pydantic_models.py

+63-9
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ class PasswordMatchException(Exception):
3434

3535

3636
class CreateUserRequest(BaseModel):
37-
# username: constr(pattern=config.USERNAME_REGEX)
38-
# password: constr(pattern=config.PASSWORD_REGEX)
39-
4037
username: str = Field(..., min_length=2, max_length=100)
4138
password: str = Field(..., min_length=8)
4239
verify_password: str = Field(..., min_length=8)
@@ -64,12 +61,6 @@ def passwords_match(cls, data: Any) -> Any:
6461
raise ValueError('Passwords do not match')
6562
return data
6663

67-
# def _passwords_match(self):
68-
# if password == verify_password:
69-
# # raise ValueError("Passwords do not match")
70-
# return True
71-
# return False
72-
7364
# Example form configuration with default values set
7465
example_form_config = {
7566
"example_form": {
@@ -282,6 +273,69 @@ class Config:
282273

283274
return models
284275

276+
277+
278+
def get_form_names(config_path=config.FORM_CONFIG_PATH):
279+
"""
280+
Given a form config path, return a list of available forms, defaulting to the example
281+
dictionary provided above.
282+
"""
283+
# Try to open config_path and if not existent or empty, use example config
284+
form_config = example_form_config # Default to example_form_config
285+
286+
if os.path.exists(config_path):
287+
try:
288+
with open(config_path, 'r') as file:
289+
form_config = json.load(file)
290+
except json.JSONDecodeError:
291+
pass
292+
# print("Failed to load the JSON file. Falling back to the default configuration.")
293+
else:
294+
pass
295+
# print("Config file does not exist. Using the default configuration.")
296+
return form_config.keys()
297+
298+
def get_form_config(form_name, config_path=config.FORM_CONFIG_PATH):
299+
300+
# Try to open config_path and if not existent or empty, use example config
301+
form_config = example_form_config # Default to example_form_config
302+
303+
if os.path.exists(config_path):
304+
try:
305+
with open(config_path, 'r') as file:
306+
form_config = json.load(file)
307+
except json.JSONDecodeError:
308+
pass
309+
# print("Failed to load the JSON file. Falling back to the default configuration.")
310+
else:
311+
pass
312+
# print("Config file does not exist. Using the default configuration.")
313+
314+
if form_name not in form_config:
315+
raise Exception(f"Form '{form_name}' not found in")
316+
317+
fields = form_config[form_name]
318+
field_definitions = {}
319+
320+
for field_name, field_info in fields.items():
321+
python_type: Type = field_info["output_type"]
322+
default = field_info.get("default", ...)
323+
324+
# Ensure Optional is always used with a specific type
325+
if default is ... and python_type != Optional:
326+
python_type = Optional[python_type]
327+
328+
field_definitions[field_name] = (python_type, default)
329+
330+
# Creating the model dynamically, allowing arbitrary types
331+
class Config:
332+
arbitrary_types_allowed = True
333+
334+
model = create_model(form_name, __config__=Config, **field_definitions)
335+
336+
return model
337+
338+
285339
def __reconstruct_form_data(request, form_fields):
286340
"""
287341
This repackages request data into a format that pydantic will be able to understand.

0 commit comments

Comments
 (0)