Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Refactor Config Module #87

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

Killg0d
Copy link
Contributor

@Killg0d Killg0d commented Dec 27, 2024

Description

This PR refactors the Config module of the boilerplate to improve its readability, maintainability, and functionality. It removes the existing service manager and introduces enhancements inspired by the MERN boilerplate.

Changes

  • Removed the existing service manager.
  • Integrated pyaml for reading .yml configuration files.
  • Refactored ConfigService to closely align with the structure and functionality of the MERN boilerplate.

Custom Environment Variables

Some deployment scenarios require secrets and settings to be configured via environment variables instead of hardcoding them in the codebase. The custom-environment-variables.yml file introduces a YAML-based approach for mapping environment variables into your configuration.

To enable custom environment variables, create a configuration file at config/custom-environment-variables.yml, mapping environment variable names to your configuration structure. Example:

web_app_host: "WEB_APP_HOST"  

inspectlet:  
  key: "INSPECTLET_KEY"  

papertrail:  
  host: "PAPERTRAIL_HOST"  
  port:  
    __name: "PAPERTRAIL_PORT"  
    __format: "number"  

This example checks for the environment variables WEB_APP_HOST and INSPECTLET_KEY to override corresponding config values. The PAPERTRAIL_PORT variable is parsed using the specified __format type (number in this case).

Available __format types:

  • boolean
  • number

Precedence: Custom environment variables override all other configuration files, including default.yml and {app_env}.yml.

Database Schema Changes

N/A

Tests

Automated Tests

  • All automated test cases passed.

Manual Test Cases

User Creation

  • Verified document generation when creating a user using both username/password and phone number/OTP.

Login

  • Verified login functionality using username credentials.
  • Verified login functionality using phone number and OTP.

OTP Errors

  • Invalid OTP: Raised HTTP error 400 (code: OTP_ERR_01, "invalid otp").
  • Expired OTP: Raised HTTP error 400 (code: OTP_ERR_02, "expired otp").

Account Errors

  • Incorrect email during login: Raised HTTP error 404 (code: ACCOUNT_ERR_02, "account not found").
  • Incorrect password during login: Raised HTTP error 403 (code: ACCOUNT_ERR_02, "invalid password").
  • Incorrect email during password reset: Raised HTTP error 404 (code: ACCOUNT_ERR_02, "account not found").
  • Existing email during signup: Raised HTTP error 409 (code: ACCOUNT_ERR_01, "account already exists").

@Killg0d Killg0d requested a review from a team December 27, 2024 09:38
@Killg0d Killg0d force-pushed the tarun/refactor/frm-18-refactor-config-module branch from 0840424 to ecec2ff Compare December 27, 2024 10:22
@Killg0d Killg0d self-assigned this Dec 27, 2024
@Killg0d Killg0d added the chore Housekeeping label Dec 27, 2024
@Killg0d Killg0d linked an issue Dec 27, 2024 that may be closed by this pull request

This comment has been minimized.

This comment has been minimized.

config/development.ini Outdated Show resolved Hide resolved
src/apps/backend/modules/config/config_service.py Outdated Show resolved Hide resolved

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@govind-io
Copy link
Contributor

The current Config service does not seems to be similar as the one that we are using in mern-boilerplate We should expose just two methods from this class getValue and hasValue both of these functions should be type safe using generic types.

e.g:

def get_value(key: str, expected_type: Type[T]) -> T:
       # actual function logic here

# example usage
ConfigUtil.get_value("string_key", str)
ConfigUtil.get_value("integer_key", int)

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@Killg0d Killg0d force-pushed the tarun/refactor/frm-18-refactor-config-module branch from 24b2764 to 73b87ea Compare January 6, 2025 14:58

This comment has been minimized.

@Killg0d Killg0d force-pushed the tarun/refactor/frm-18-refactor-config-module branch from 73b87ea to 38a23b5 Compare January 6, 2025 15:02

This comment has been minimized.

This comment has been minimized.

@Killg0d Killg0d changed the title Chore: Refactor Config Module [WIP]-Chore: Refactor Config Module Jan 7, 2025

This comment has been minimized.

removed unused settings and config_manager
modified config_service to be the class that handles configuration
modified all instances of config_manager to use config_service
- Overridden optionxform in ConfigParser to preserve the case of keys
- Ensured environment variables are matched with original case from .ini files
refactor: created a config file which simulates node-config
refactor: modified config_service to extend config so that it reflects mern-boilerplate
@Killg0d Killg0d force-pushed the tarun/refactor/frm-18-refactor-config-module branch from 5d5b7e4 to 817c48f Compare January 30, 2025 05:10

This comment has been minimized.

This comment has been minimized.

@Killg0d Killg0d requested a review from a team January 30, 2025 05:13
config/custom-environment-variables.yml Show resolved Hide resolved
config/default.yml Outdated Show resolved Hide resolved
import yaml


def get_parent_directory(directory: str, levels: int) -> Path:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid creating methods outside class


class Config:
config_dict: dict[str, Any]
config_path: Path = get_parent_directory(__file__, 6) / "config"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid magic numbers like 6 which are not readable.

return parent_dir


class Config:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is not supposed to be file consumed by other modules directly, this should live inside internals folder

value = Config.get(key)
if value is None:
raise MissingKeyError(missing_key=key, error_code=ErrorCode.MISSING_KEY)
return value # type: ignore
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this?

How are we enforcing type safety? For example if some asked for bool but found a string, that should throw.

return yaml_content

@staticmethod
def load_config() -> None:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If this is the only public method, move to top of the class.
  2. Mark all internal methods to start with _
  3. Consider improving readability of the code overall from code organization, naming and others

This comment has been minimized.

This comment has been minimized.

Copy link

Passed

Analysis Details

0 Issues

  • Bug0 Bugs
  • Vulnerability0 Vulnerabilities
  • Code Smell0 Code Smells

Coverage and Duplications

  • No coverage informationNo coverage information (0.00% Estimated after merge)
  • 3 percent duplication0.00% Duplicated Code (0.50% Estimated after merge)

Project ID: jalantechnologies_rflask-boilerplate

View in SonarQube

Copy link

Deployment (frm-boilerplate) is available at - https://f55a0c2629bdc00.preview.platform.jalantechnologies.com

Copy link

Code Coverage

Package Line Rate Health
src.apps.backend 97%
src.apps.backend.bin 70%
src.apps.backend.modules 100%
src.apps.backend.modules.access_token 99%
src.apps.backend.modules.access_token.rest_api 100%
src.apps.backend.modules.account 99%
src.apps.backend.modules.account.internal 98%
src.apps.backend.modules.account.internal.store 97%
src.apps.backend.modules.account.rest_api 100%
src.apps.backend.modules.application 93%
src.apps.backend.modules.common 89%
src.apps.backend.modules.communication 81%
src.apps.backend.modules.communication.internals 48%
src.apps.backend.modules.config 100%
src.apps.backend.modules.config.internals 89%
src.apps.backend.modules.error 85%
src.apps.backend.modules.logger 86%
src.apps.backend.modules.logger.internal 79%
src.apps.backend.modules.otp 100%
src.apps.backend.modules.otp.internal 91%
src.apps.backend.modules.otp.internal.store 97%
src.apps.backend.modules.password_reset_token 100%
src.apps.backend.modules.password_reset_token.internal 97%
src.apps.backend.modules.password_reset_token.internal.store 100%
src.apps.backend.modules.password_reset_token.rest_api 100%
tests 100%
tests.modules 100%
tests.modules.access_token 99%
tests.modules.account 100%
tests.modules.common 90%
tests.modules.config 100%
tests.modules.password_reset_token 100%
Summary 93% (1688 / 1818)

Minimum allowed line rate is 60%


T = TypeVar('T')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to bound the values of T?

def has_key(key: str) -> bool:
return key in ConfigManager.config
@classmethod
def get_value(cls,key: str) -> T:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add space after ,

def load_config() -> None:
Config._intialize_config()
Config._process_custom_environment_variables()
print(yaml.dump(Config._config_dict))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please avoid this?


@staticmethod
def get(key: str, default: Optional[Any] = None) -> Any:
keys = key.split(".")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using magic string or number. You can make this code readable by storing . in a constant variable, something of the form - CONFIG_KEY_SEPARATOR or something to that effect.


@staticmethod
def get(key: str, default: Optional[Any] = None) -> Any:
keys = key.split(".")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend to use better naming convention here, and below. What you have fully qualified namespace for the key.

for k in keys:
value = value[k]
return value
except (KeyError, TypeError):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add comments in the code when system would throw these errors (under what circumstances)?

Comment on lines +30 to +34
keys = key.split(".")
value = Config._config_dict
try:
for k in keys:
value = value[k]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First 5 lines is repeating here, and in get method. Consider refactoring.

Config._config_dict = merge_content

@staticmethod
def _parse_value(value:Optional[str], value_format:str)-> Any:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: add space after :

Kindly scan your code to ensure we are using spacing properly.

Config._config_dict = merge_content

@staticmethod
def _parse_value(value:Optional[str], value_format:str)-> Any:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this need be converted to use generics?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is still a lot of code in this file which is hard to follow and its flow. There are opportunity to think through this and break into appropriate entities, encapsulate behaviour, possibly using OOPS. This is largely created using functional programming and its hard to follow all the functions and how and when they are called, which is making them hard to read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
chore Housekeeping
Projects
None yet
Development

Successfully merging this pull request may close these issues.

chore: Refactor Config module
3 participants