Skip to content

Commit

Permalink
Merge pull request #1 from Jmateusribeiro/pylint
Browse files Browse the repository at this point in the history
Pylint
  • Loading branch information
Jmateusribeiro authored Jun 2, 2024
2 parents 4e0ec11 + 96905ca commit b7d2768
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 119 deletions.
24 changes: 24 additions & 0 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Pylint

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pylint
pip install pytest
- name: Analyzing the code with pylint
run: |
pylint --disable=logging-fstring-interpolation,broad-exception-caught,broad-exception-raised $(git ls-files '*.py')
Binary file removed dist/folder_synchronizer-0.0.1-py3-none-any.whl
Binary file not shown.
Binary file removed dist/folder_synchronizer-0.0.1.tar.gz
Binary file not shown.
45 changes: 26 additions & 19 deletions folder_synchronizer/__main__.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,39 @@
"""
main module
main module
"""

import argparse
import os
from folder_synchronizer.classes.log import CustomLogger
from folder_synchronizer.classes.folder_operations import FolderSynchronizer

def parse_args() -> argparse.Namespace:
"""
Parses command-line arguments.
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--sourceDir",
type=str,
help="Complete Directory of Source folder",
Returns:
argparse.Namespace: Parsed command-line arguments.
"""
parser = argparse.ArgumentParser(description="Synchronize folders and log the process.")
parser.add_argument("--sourceDir",
type=str,
help="Complete Directory of Source folder",
required=True)
parser.add_argument("--replicaDir",
type=str,
help="Complete Directory of Replica folder",
parser.add_argument("--replicaDir",
type=str,
help="Complete Directory of Replica folder",
required=True)
parser.add_argument("--logFolder",
type=str,
help="Complete Directory of Log folder",
parser.add_argument("--logFolder",
type=str,
help="Complete Directory of Log folder",
required=True)
return parser.parse_args()

def main():
def main() -> None:
"""
Main function to synchronize source and replica folders and log the process.
"""
args = parse_args()

source_folder = args.sourceDir
replica_folder = args.replicaDir
logs_folder = args.logFolder
Expand All @@ -39,15 +46,15 @@ def main():
log.info("### Folder Synchronization ###")
log.info(f"Source Folder: {source_folder}")
log.info(f"Replica Folder: {replica_folder}")
folder_sync = FolderSynchronizer(source_folder, replica_folder, log)
folder_sync = FolderSynchronizer(source_folder, replica_folder, log)

try:
log.info("...Strating Synchronization...")
log.info("...Starting Synchronization...")
folder_sync.synchronize()
log.info("...Folders Synchronized...")
except Exception as e:
log.error(f"Error during synchronization: {str(e)}")

log.info("...Ending Synchronization...")

if __name__ == "__main__":
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
84 changes: 50 additions & 34 deletions folder_synchronizer/classes/folder_operations.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,56 @@
"""
folder scynchronizer class
Folder Synchronizer class for synchronizing contents between source and replica folders.
"""
import os
import filecmp
import shutil
from pathlib import Path


class FolderSynchronizer:
def __init__(self, source_folder, replica_folder, log):
# using 'Path' class to facilitate (method rglob) iteration over all subfiles and directories.
"""
A class to synchronize contents between a source folder and a replica folder.
Attributes:
source_folder (Path): Path to the source folder.
replica_folder (Path): Path to the replica folder.
log (CustomLogger): CustomLogger instance for logging synchronization activities.
"""

def __init__(self, source_folder: str, replica_folder: str, log: 'CustomLogger') -> None:
"""
Initializes the FolderSynchronizer with the given folders and logger.
Args:
source_folder (str): The path to the source folder.
replica_folder (str): The path to the replica folder.
log (CustomLogger): CustomLogger instance for logging synchronization activities.
"""
self.source_folder = Path(source_folder)
self.replica_folder = Path(replica_folder)
self.log = log

def synchronize(self):
# If source folder don't exists should be thrown error
# but if replica folder don't exists must be created
def synchronize(self) -> None:
"""
Synchronizes the contents of the source folder to the replica folder.
Raises:
FileNotFoundError: If the source folder does not exist.
"""
if not os.path.exists(self.source_folder):
raise FileNotFoundError(f"Source folder not found: {self.source_folder}")

if not os.path.exists(self.replica_folder):
os.makedirs(self.replica_folder)
# logging that action has a warning because
# this folder is created when the program starts, by default
self.log.warn(f"Replica folder was deleted: {self.replica_folder}")
self.log.warning(f"Replica folder was created: {self.replica_folder}")

self.__remove_items()
self.__create_or_update_items()
self.remove_items()
self.create_or_update_items()

# since this class it will not be inherited (at least in this challenge)
# methods from here will be private (and not protected)
def __remove_items(self):
def remove_items(self) -> None:
"""
Removes items from the replica folder that do not exist in the source folder.
"""
for replica_item_path in self.replica_folder.rglob('*'):
# it could be used 'Path' methods to perform the actions over files and folder
# but I prefer using 'os' methods, since 'Path' methods use 'os' anyway
source_item_path = os.path.join(self.source_folder,
source_item_path = os.path.join(self.source_folder,
os.path.relpath(replica_item_path, self.replica_folder))
try:
if not os.path.exists(source_item_path):
Expand All @@ -47,26 +62,28 @@ def __remove_items(self):
self.log.info(f"Removed Folder: {replica_item_path}")

except Exception as e:
raise Exception(f"Error removing '{replica_item_path}' - {str(e)}")

def __create_or_update_items(self):
raise Exception(f"Error removing '{replica_item_path}' - {str(e)}") from e

def create_or_update_items(self) -> None:
"""
Creates or updates items in the replica folder to match the source folder.
"""
for source_item_path in self.source_folder.rglob('*'):
replica_item_path = os.path.join(self.replica_folder,
replica_item_path = os.path.join(self.replica_folder,
os.path.relpath(source_item_path, self.source_folder))

if os.path.isdir(source_item_path):
if not os.path.exists(replica_item_path):
os.makedirs(replica_item_path)
self.log.info(f"Created folder: {replica_item_path}")
self.log.info(f"Created folder: {replica_item_path}")

elif os.path.isfile(source_item_path):
try:
# If file already exists and has the same content, should go to next iteration
if os.path.exists(replica_item_path) and filecmp.cmp(source_item_path, replica_item_path):
if os.path.exists(replica_item_path) and filecmp.cmp(source_item_path,
replica_item_path, shallow=False):
continue

if os.path.exists(source_item_path):

#Logging different variables just because a matter of logic :)
#Update item inside replica folder but copied from source folder
if os.path.exists(replica_item_path):
Expand All @@ -75,14 +92,13 @@ def __create_or_update_items(self):
message = f"Copied file: {source_item_path}"

shutil.copy2(source_item_path, replica_item_path)

# logging after the action was really performed
# but need to "catch" the action before
# but need to "catch" the action before
# to understand if was a create or update action
self.log.info(message)

else:
self.log.error(f"Error: Source file not found: {source_item_path}")
except Exception as e:
raise Exception(f"Error creatig item '{source_item_path}' - {str(e)}")

error_msg = f"Error creating or updating item '{source_item_path}' - {str(e)}"
raise Exception(error_msg) from e
34 changes: 29 additions & 5 deletions folder_synchronizer/classes/log.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
"""
A custom logger class that logs messages to both console and file with timed rotation.
"""
import logging
import re
from logging import handlers, Logger

import os

class CustomLogger(Logger):
"""
A custom logger class that logs messages to both console and file with timed rotation.
This class extends the standard Logger class
from the logging module and provides additional functionality
for logging messages to both console and file.
Log files are rotated daily, and old log files are automatically
deleted after a specified number of days.
Attributes:
log_folder (str): The directory where log files will be stored.
backupCount_days (int): The number of days to keep backup log files.
Methods:
__init__: Initializes the custom logger.
"""
def __init__(self, log_folder: str, backupCount_days: int = 30) -> None:
"""
Initializes the custom logger.
def __init__(self, log_folder, backupCount_days=30):
Args:
log_folder (str): The directory where log files will be stored.
backupCount_days (int): The number of days to keep backup log files.
"""
super().__init__('CustomLogger')
self.setLevel(logging.DEBUG)

Expand All @@ -18,14 +43,13 @@ def __init__(self, log_folder, backupCount_days=30):
self.addHandler(console_handler)

# File handler (with timed rotation each day)
# Think is better to have a log file per day
file_handler = handlers.TimedRotatingFileHandler(
log_folder + '\\Folder_Sync',
os.path.join(log_folder, 'Folder_Sync'),
when='midnight',
backupCount=backupCount_days
)
file_handler.setLevel(logging.DEBUG)
file_handler.suffix = '%Y_%m_%d.log'
file_handler.extMatch = re.compile(r"^\d{4}_\d{2}_\d{2}.log$")
file_handler.setFormatter(formatter)
self.addHandler(file_handler)
self.addHandler(file_handler)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from setuptools import setup, find_packages

with open('requirements.txt') as f:
with open('requirements.txt', encoding='utf-8') as f:
requirements = f.read().splitlines()

setup(
Expand Down
Loading

0 comments on commit b7d2768

Please sign in to comment.