-
Notifications
You must be signed in to change notification settings - Fork 8
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
Refactor Agency Configuration File Handling and Add Tests #10
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f8f10aa
Add AgencyConfigStorage
bonk1t 1b0e01a
Add AgencyConfigStorage
bonk1t d6183e3
Add tests
bonk1t 592a3f3
Resolve merge conflicts
bonk1t d10576f
Update authors in pyproject.toml
bonk1t 39ba4ce
Update authors in pyproject.toml
bonk1t 236b017
Merge branch 'main' into feat/refactor-agency-config
bonk1t File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import json | ||
from pathlib import Path | ||
|
||
from nalgonda.constants import CONFIG_FILE_BASE, DEFAULT_CONFIG_FILE | ||
from nalgonda.persistence.agency_config_lock_manager import AgencyConfigLockManager | ||
from nalgonda.persistence.agency_config_storage_interface import AgencyConfigStorageInterface | ||
|
||
|
||
class AgencyConfigFileStorage(AgencyConfigStorageInterface): | ||
""" | ||
A thread-safe context manager for handling agency-specific configuration files. | ||
|
||
This class ensures that file operations on configuration files are managed | ||
in a thread-safe manner using locks. Each agency identified by its unique ID | ||
gets its own lock to prevent concurrent access issues. | ||
""" | ||
|
||
def __init__(self, agency_id: str): | ||
self.config_file_path = self._get_config_path(agency_id) | ||
self.lock = None | ||
self.agency_id = agency_id | ||
|
||
def __enter__(self): | ||
""" | ||
Enters the runtime context and acquires the lock for the agency file. | ||
|
||
If the specific agency configuration file does not exist, it falls back | ||
to a default configuration file. | ||
|
||
Returns: | ||
self: An instance of AgencyConfigFileStorage. | ||
""" | ||
self.lock = AgencyConfigLockManager.get_lock(self.agency_id) | ||
self.lock.acquire() | ||
if not self.config_file_path.is_file(): | ||
self._create_default_config() | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
""" | ||
Exits the runtime context and releases the lock for the agency file. | ||
""" | ||
self.lock.release() | ||
|
||
def load(self): | ||
""" | ||
Loads the configuration from the agency file. | ||
|
||
Returns: | ||
dict: The loaded configuration data. | ||
""" | ||
with self.config_file_path.open() as file: | ||
return json.load(file) | ||
|
||
def save(self, data): | ||
""" | ||
Saves the provided data to the agency file. | ||
|
||
Args: | ||
data (dict): The configuration data to be saved. | ||
""" | ||
with self.config_file_path.open("w") as file: | ||
json.dump(data, file, indent=2) | ||
|
||
def _create_default_config(self): | ||
""" | ||
Creates a new configuration file based on the default configuration. | ||
""" | ||
with DEFAULT_CONFIG_FILE.open() as file: | ||
config = json.load(file) | ||
self.save(config) | ||
|
||
@staticmethod | ||
def _get_config_path(agency_id: str) -> Path: | ||
""" | ||
Generates the path for the agency configuration file. | ||
|
||
Args: | ||
agency_id (str): The unique identifier for the agency. | ||
|
||
Returns: | ||
Path: The path object for the agency configuration file. | ||
""" | ||
return CONFIG_FILE_BASE.with_name(f"config_{agency_id}.json") |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from abc import ABC, abstractmethod | ||
|
||
|
||
class AgencyConfigStorageInterface(ABC): | ||
"""Interface for agency config storage""" | ||
|
||
@abstractmethod | ||
def __enter__(self): | ||
"""Enter context manager""" | ||
pass | ||
|
||
@abstractmethod | ||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
"""Exit context manager""" | ||
pass | ||
|
||
@abstractmethod | ||
def load(self): | ||
"""Load agency config from the storage""" | ||
pass | ||
|
||
@abstractmethod | ||
def save(self, data): | ||
"""Save agency config to the storage""" | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ version = "1.0.0" | |
description = "Nalgonda is a FastAPI app to manage swarm agencies" | ||
authors = [ | ||
"Nikita Bobrovskiy <[email protected]>", | ||
"Guilherme Parpinelli <[email protected]>", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😀 |
||
] | ||
readme = "README.md" | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import json | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
|
||
from nalgonda.constants import DEFAULT_CONFIG_FILE | ||
from nalgonda.persistence.agency_config_file_storage import AgencyConfigFileStorage | ||
from nalgonda.persistence.agency_config_lock_manager import AgencyConfigLockManager | ||
|
||
|
||
@pytest.fixture | ||
def mock_config_path(temp_dir): | ||
"""Fixture to patch the _get_config_path method in AgencyConfigFileStorage.""" | ||
agency_id = "test_agency" | ||
config_path = temp_dir / f"config_{agency_id}.json" | ||
|
||
with patch.object(AgencyConfigFileStorage, "_get_config_path", return_value=config_path): | ||
yield config_path, agency_id | ||
|
||
|
||
def test_lock_acquisition_and_release(mock_config_path): | ||
_, agency_id = mock_config_path | ||
storage = AgencyConfigFileStorage(agency_id) | ||
lock = AgencyConfigLockManager.get_lock(agency_id) | ||
|
||
with storage: | ||
assert lock.locked(), "Lock was not acquired" | ||
|
||
assert not lock.locked(), "Lock was not released" | ||
|
||
|
||
def test_load_configuration(mock_config_path): | ||
config_path, agency_id = mock_config_path | ||
config_data = {"key": "value"} | ||
config_path.write_text(json.dumps(config_data)) | ||
|
||
storage = AgencyConfigFileStorage(agency_id) | ||
with storage: | ||
loaded_config = storage.load() | ||
|
||
assert loaded_config == config_data, "Loaded configuration does not match expected data" | ||
|
||
|
||
def test_save_configuration(mock_config_path): | ||
config_path, agency_id = mock_config_path | ||
new_config = {"new_key": "new_value"} | ||
|
||
storage = AgencyConfigFileStorage(agency_id) | ||
with storage: | ||
storage.save(new_config) | ||
|
||
assert config_path.exists(), "Configuration file was not created" | ||
with open(config_path) as file: | ||
saved_data = json.load(file) | ||
assert saved_data == new_config, "Saved data does not match" | ||
|
||
|
||
def test_default_configuration_used(mock_config_path): | ||
config_path, agency_id = mock_config_path | ||
|
||
with open(DEFAULT_CONFIG_FILE) as file: | ||
default_config = json.load(file) | ||
|
||
# Ensure the specific config file does not exist to trigger default config usage | ||
if config_path.exists(): | ||
config_path.unlink() | ||
|
||
storage = AgencyConfigFileStorage(agency_id) | ||
with storage: | ||
loaded_config = storage.load() | ||
|
||
assert loaded_config == default_config, "Default configuration was not used" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import threading | ||
|
||
from nalgonda.persistence.agency_config_lock_manager import AgencyConfigLockManager | ||
|
||
|
||
def test_lock_uniqueness(): | ||
agency_id1 = "agency1" | ||
agency_id2 = "agency2" | ||
|
||
lock1 = AgencyConfigLockManager.get_lock(agency_id1) | ||
lock2 = AgencyConfigLockManager.get_lock(agency_id2) | ||
|
||
assert lock1 is not lock2, "Different agencies should have different locks" | ||
|
||
|
||
def test_lock_for_same_agency(): | ||
agency_id = "agency1" | ||
|
||
lock1 = AgencyConfigLockManager.get_lock(agency_id) | ||
lock2 = AgencyConfigLockManager.get_lock(agency_id) | ||
|
||
assert lock1 is lock2, "The same agency should return the same lock instance" | ||
|
||
|
||
def test_lock_concurrency_handling(): | ||
agency_id = "agency1" | ||
lock = AgencyConfigLockManager.get_lock(agency_id) | ||
|
||
# Define a shared resource | ||
shared_resource = [] | ||
|
||
def task(): | ||
with lock: | ||
shared_resource.append(1) | ||
|
||
# Run tasks in parallel to simulate concurrent access | ||
threads = [threading.Thread(target=task) for _ in range(10)] | ||
for thread in threads: | ||
thread.start() | ||
for thread in threads: | ||
thread.join() | ||
|
||
assert len(shared_resource) == 10, "Concurrency issue: shared resource was accessed simultaneously" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original plan for the context manager implementation was to eliminate the need for a separate lock manager class. Instead, the context manager itself would handle the lock. This means that upon entering the context (i.e., during the enter method), it would acquire the lock and then return the opened file. Conversely, upon exiting the context (exit method), it would release the lock and close the file.