Repository: JuanARojasA/dataflat (language: python)

Issue:
Dev



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/python-analyze-code.yml b/.github/workflows/python-analyze-code.yml
new file mode 100644
index 0000000..fff1c84
--- /dev/null
+++ b/.github/workflows/python-analyze-code.yml
@@ -0,0 +1,20 @@
+# This workflow will upload a Python Package using Twine when a release is created
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Analyze code on PR
+on:
+  pull_request:
+    types: [ opened, synchronize, reopened ]
+permissions:
+  contents: read
+
+jobs:
+  analysis:
+    uses: ./.github/workflows/sonar_code_quality.yml
+    secrets:
+      SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/python-publish-dev.yml b/.github/workflows/python-publish-dev.yml
index 5a2da49..ba91b11 100644
--- a/.github/workflows/python-publish-dev.yml
+++ b/.github/workflows/python-publish-dev.yml
@@ -6,36 +6,37 @@
 # separate terms of service, privacy policy, and support
 # documentation.
 
-name: Publish Python 🐍 distributions 📦 to PyPI TestPyPI
-
+name: Publish Python 🐍 distributions 📦 to TestPyPI
 on:
   push:
     branches: 
-     - 'dev'
-
+     - dev
 permissions:
   contents: read
 
 jobs:
+  analysis:
+    uses: ./.github/workflows/sonar_code_quality.yml
+    secrets:
+      SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
   deploy:
-
+    needs: [analysis]
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v3
-    - name: Set up Python
-      uses: actions/setup-python@v3
-      with:
-        python-version: '3.x'
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install build
-    - name: Build package
-      run: python -m build
-    - name: Publish package
-      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
-      with:
-        user: __token__
-        password: ${{ secrets.TEST_PYPI_API_TOKEN }}
-        repository_url: https://test.pypi.org/legacy/
+      - uses: actions/checkout@v4.1.7
+      - name: Set up Python
+        uses: actions/setup-python@v5.2.0
+        with:
+          python-version: '3.10'
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build
+      - name: Build package
+        run: python -m build
+      - name: Publish package
+        uses: pypa/gh-action-pypi-publish@v1.10.1
+        with:
+          user: __token__
+          password: ${{ secrets.TEST_PYPI_API_TOKEN }}
+          repository_url: https://test.pypi.org/legacy/
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index b6715dc..9fdc920 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -7,26 +7,27 @@
 # documentation.
 
 name: Publish Python 🐍 distributions 📦 to PyPI
-
 on:
   push:
     branches: 
-     - 'master'
-
+     - master
 permissions:
   contents: read
 
 jobs:
+  analysis:
+    uses: ./.github/workflows/sonar_code_quality.yml
+    secrets:
+      SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
   deploy:
-
+    needs: [analysis]
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4.1.7
     - name: Set up Python
-      uses: actions/setup-python@v3
+      uses: actions/setup-python@v5.2.0
       with:
-        python-version: '3.x'
+        python-version: '3.10'
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
@@ -34,7 +35,7 @@ jobs:
     - name: Build package
       run: python -m build
     - name: Publish package
-      uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
+      uses: pypa/gh-action-pypi-publish@v1.10.1
       with:
         user: __token__
         password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/.github/workflows/sonar_code_quality.yml b/.github/workflows/sonar_code_quality.yml
new file mode 100644
index 0000000..c0f5c9f
--- /dev/null
+++ b/.github/workflows/sonar_code_quality.yml
@@ -0,0 +1,45 @@
+name: Sonar
+on:
+  workflow_call:
+    secrets:
+      SONAR_TOKEN:
+        required: true
+
+jobs:
+  tests:
+    name: Testing
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4.1.7
+      - name: Set up Python
+        uses: actions/setup-python@v3
+        with:
+          python-version: '3.10'
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install .[test]
+      - name: Run tests and generate coverage report
+        run: |
+          coverage run -m pytest
+          coverage xml
+      - name: Upload coverage report
+        uses: actions/upload-artifact@v4.4.0
+        with:
+          name: coverage-report
+          path: coverage.xml
+  sonarcloud:
+    needs: [tests]
+    name: SonarCloud
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4.1.7
+        with:
+          fetch-depth: 0
+      - name: Download coverage reports
+        uses: actions/download-artifact@v4.1.8
+      - name: SonarCloud Scan
+        uses: SonarSource/sonarcloud-github-action@master
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index f754f44..707a105 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,272 @@
-*.zip
-*.DS_Store
-bdist.macosx*
-dist*
-lib
-dataflat.egg-info
\ No newline at end of file
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# SonarLint plugin
+.idea/sonarlint/
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### macOS template
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Python template
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.idea/
+
+# local testing with jupyter
+*test.ipynb
+*test.json
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..41062d5
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,10 @@
+exclude: '.git'
+default_stages: [commit]
+fail_fast: true
+
+repos:
+    - repo: https://github.com/commitizen-tools/commitizen
+      rev: v3.29.0
+      hooks:
+          - id: commitizen
+            stages: [commit-msg]
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e6fde19
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,22 @@
+## v2.0.0 (2024-09-06)
+
+### BREAKING CHANGE
+
+- deprecate pandas flattener,change variable names and delete unused variables from function calls
+
+### Fix
+
+- **dataflat**: fix error with spark and dictionary flattener, deprecate pandas flattener
+
+## v1.1.2 (2024-09-03)
+
+### Fix
+
+- **base/flattener.py**: fix problem with flatten method, it should be abstract
+- **commons.py**: a fix was made for a problem with "| None"
+
+## v1.1.1 (2024-09-02)
+
+### Refactor
+
+- **dictionary/flattener.py**: fix sonarcloud issue with function complexity
\ No newline at end of file
diff --git a/README.md b/README.md
index ef329df..5608135 100644
--- a/README.md
+++ b/README.md
@@ -3,48 +3,120 @@ A library to flatten all this annoiyng nested keys and columns on Dictionaries,
 and Spark (pyspark) Dataframes.
 
 ### Installation
-```
+```bash
 pip install dataflat
 ```
 
 ### Get started
 How to instantiate a Flattener:
 
-First import the FlattenerOptions, CaseTranslator and Flattener classes
-```Python
-from dataflat.flattener_handler import FlattenerOptions, CaseTranslatorOptions, Flattener
-```
+1. Import CaseTranslatorOptions, and FlattenerOptions
+
+In this step we define the case translator used to convert keys, or column names after the flattening process.
 
-The following step is assing the required variables for the flattening process.
-* reference_name: Used to assign a key name to each resulting dictionary or DataFrame from the transform function.
-* id_key: Used as identifier value in case there is any nested list, that will be exploded to a new entire dictionary or dataframe, the exploded Dataframes will have a reference to it parent Dataframe or dictionary.
-* black_list: A list of keys or columns that will be skipped during the flattening process.
-* replace_dots: Specify if the dots (used as hierarchical separator) will be replaced with underscores.
-* from_case, to_case: Specify the keys or columns current case (snake, camel...) and the desired output case for each key.
+It's necessary to select the required Flattener, for example DICTIONARY or PYSPARK_DF (more coming...)
+Secondly it's optional to select a from_case and a to_case option, for example SNAKE and CAMEL respectively.
+Finally we need to set a replace string, this is the string used to indicate the nested dependency, for example
+client.id or item.price, also we can specify if it's needed to remove special characters like '@' or '|'
+keys or column names.
 
 ```Python
+from dataflat.flattener_handler import CaseTranslatorOptions, FlattenerOptions
+
 # Default values:
-#   from_case = CaseTranslatorOptions.CAMEL_CASE
-#   to_case = CaseTranslatorOptions.SNAKE_CASE
-#   replace_dots = False
-#   chunk size = 500
-reference_name = 'data'
-id_key = 'id'
-black_list = ['keys','or','columns','to','skip']
-replace_dots = False
-from_case = CaseTranslatorOptions.CAMEL_CASE
-to_case = CaseTranslatorOptions.SNAKE_CASE
+#   from_case = None
+#   to_case = None
+#   replace_string = "."
+#   remove_special_chars = False
+custom_flattener = FlattenerOptions.DICTIONARY
+from_case = CaseTranslatorOptions.SNAKE
+to_case = CaseTranslatorOptions.CAMEL
+replace_string = "."
+remove_special_chars = False
 ```
 
-Later, select the desired flattener from the options, according to the current type of your data (dict, pandas.DataFrame, spark.DataFrame)
+After that we can proceed to instantiate a flattener using the handler, and passing it the variables defined before, and 
+flatten some data.
+**All CustomFlattener receive the same parameters on the flatten function.**
 ```Python
-custom_flattener = FlattenerOptions.DICTIONARY
-custom_flattener = FlattenerOptions.PANDAS_DF
-custom_flattener = FlattenerOptions.SPARK_DF
+from dataflat.flattener_handler import handler
+
+flattener = handler(
+    custom_flattener=custom_flattener,
+    from_case=from_case,
+    to_case=to_case,
+    replace_string=replace_string,
+    remove_special_chars=remove_special_chars
+)
+
+
+# Default values:
+#   entity_name = "data"
+#   primary_key = "id
+#   partition_keys = []
+#   black_list = []
+data={}
+entity_name = "data"
+primary_key = "id"
+partition_keys = ["date"]
+black_list = ['keys.or', 'columns', 'to.be.ignored']
+flatten_data = flattener.flatten(
+    data=data,
+    primary_key=primary_key, 
+    entity_name=entity_name,
+    partition_keys=partition_keys,
+    black_list=black_list
+)
+```
+* ```primary_key```: Used to connect dictionaries or dataframes with the "parent" dataframe, for example
+```id```, its required that the dictionary or dataframe contains a 'primary_key' that can be propagated to 'child'.
+* ```partition_keys```: List of keys that you must want to propagate to nested data, for example "date" key/column.
+* ```black_list```: List of keys that must me ignored/skipped during the flattening process, this keys will not be
+present on the flatten_data.
+* ```flatten_data```: A dictionary with one or multiples keys, one for the "parent" and much for the "child".
+Each list or array inside the "original" data will result on a key on this dictionary, an example of flatten_data could be:
+```json
+{
+  "data": [{"id": 1, "date": "2024-01-01", "total": 1900}],
+  "data.orders": [
+    {"id": "abc123", "total": 700, "data.id": 1, "data.date": "2024-01-01", "index": 0},
+    {"id": "dfg456", "total": 1200, "data.id": 1, "data.date": "2024-01-01", "index": 1}
+  ],
+  "data.orders.products": [
+    {"id": "ab", "price": 200, "data.id": 1, "data.date": "2024-01-01", "data.orders.index": 0, "index": 0},
+    {"id": "cd", "price": 500, "data.id": 1, "data.date": "2024-01-01", "data.orders.index": 0, "index": 1},
+    {"id": "fg", "price": 1200, "data.id": 1, "data.date": "2024-01-01", "data.orders.index": 1, "index": 0}
+  ]
+}
 ```
 
-Finally instantiate the flattener class, and apply the transform function.
-```Python
-flattener = Flattener().handler(custom_flattener, from_case, to_case, replace_dots)
-flattener_dict.transform(data, id_key, black_list, reference_name)
-```
\ No newline at end of file
+The original json that result in this data was:
+```json
+{
+  "id":  1,
+  "date": "2024-01-01",
+  "orders": [
+    {
+      "id":  "abc123",
+      "products": [
+        {"id":  "ab", "price":  200},
+        {"id":  "cd", "price":  500}
+      ],
+      "total":  700
+    },
+    {
+      "id": "dfg456",
+      "products": [
+        {"id":  "fg", "price":  1200}
+      ],
+      "total":  1200}
+  ],
+  "total":  1900
+}
+```
+
+### Recommendations
+1. For PYSPARK_DF flattener it's recommended to set the 'caseSensitive' configuration to True on Spark.
+    ```Python
+        spark.conf.set('spark.sql.caseSensitive', True)
+    ```
\ No newline at end of file
diff --git a/dataflat/pandas_df/__init__.py b/dataflat/base/__init__.py
similarity index 100%
rename from dataflat/pandas_df/__init__.py
rename to dataflat/base/__init__.py
diff --git a/dataflat/base/flattener.py b/dataflat/base/flattener.py
new file mode 100644
index 0000000..39b0a4b
--- /dev/null
+++ b/dataflat/base/flattener.py
@@ -0,0 +1,43 @@
+"""
+dataflat/base/flattener.py - The Base class for the dataflat library
+
+Copyright (C) 2024 Juan ROJAS
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Authors:
+    Juan ROJAS <jarojasa97@gmail.com>
+"""
+
+from abc import abstractmethod
+from dataclasses import dataclass
+from typing import Any, Optional
+
+from dataflat.utils.case_translator import CustomCaseTranslator
+
+
+@dataclass
+class BaseFlattener:
+    case_translator: Optional[CustomCaseTranslator] = None
+    replace_string: Optional[str] = "."
+    entity_name: Optional[str] = "data"
+    primary_key: Optional[str] = "id"
+
+    @abstractmethod
+    def flatten(
+        self,
+        data: Any,
+        primary_key: Optional[str] = None,
+        entity_name: Optional[str] = None,
+        partition_keys: Optional[list[str]] = None,
+        black_list: Optional[list[str]] = None,
+    ) -> None:
+        return None
diff --git a/dataflat/commons.py b/dataflat/commons.py
index 57feded..6f9e46a 100644
--- a/dataflat/commons.py
+++ b/dataflat/commons.py
@@ -1,11 +1,11 @@
-'''
+"""
 dataflat/commons.py - a script file for common used functions
 
 Copyright (C) 2023 Juan ROJAS
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,16 +15,19 @@
 
 Authors:
     Juan ROJAS <jarojasa97@gmail.com>
-'''
+"""
 
 import logging
+
+from typing import Optional
 from typeguard import typechecked
 
+
 @typechecked
-def init_logger(file_name:str, log_level=None):
+def init_logger(file_name: str, log_level: Optional[str] = None):
     if not log_level:
-        log_level = 'INFO'
+        log_level = "INFO"
     logging.basicConfig()
     logger = logging.getLogger(file_name)
     logger.setLevel(getattr(logging, log_level))
-    return logger
\ No newline at end of file
+    return logger
diff --git a/dataflat/dictionary/flattener.py b/dataflat/dictionary/flattener.py
index 5d13797..5b39585 100644
--- a/dataflat/dictionary/flattener.py
+++ b/dataflat/dictionary/flattener.py
@@ -1,11 +1,11 @@
-'''
+"""
 dataflat/dictionary/flattener.py - The processor script for dictionaries flattening process
 
-Copyright (C) 2023 Juan ROJAS
+Copyright (C) 2024 Juan ROJAS
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,180 +15,165 @@
 
 Authors:
     Juan ROJAS <jarojasa97@gmail.com>
-'''
+"""
 
 import re
+from collections import defaultdict
+from typing import Any, Optional
+
 from typeguard import typechecked
-from typing import Tuple, List, Any, Union
+
+from dataflat.base.flattener import BaseFlattener
 from dataflat.commons import init_logger
-from dataflat.exceptions import FlatteningException
 from dataflat.utils.case_translator import CustomCaseTranslator
-
+from dataflat.utils.string import dot_join_args
 
 logger = init_logger(__name__)
 
+
 @typechecked
-class CustomFlattener():
-    def __init__(self, case_translator:CustomCaseTranslator, replace_dots:bool):
-        logger.info("CustomFlattener for Python Dictionaries has been initiated")
-        self._case_translator = case_translator
-        self._reference_separator = "_" if replace_dots else "."
-
-
-    def _replace_dots(self, string:str):
-        """Receive a String, and replace the '.' in name with '_'
-        Parameters
-        ----------
-        string: str
-            The string to be processed.
-        Returns
-        -------
-        replaced_string: str
-        """
-        if self._reference_separator != ".":
-            string = string.replace(".", self._reference_separator)
-            string = re.sub(f"\\{self._reference_separator}\\{self._reference_separator}+", self._reference_separator, string)
-        return string
-
-
-    def _insert_child_record(self, processed_data:dict, json_name:str, key_ref:str, value:Any,
-                             nested_index:Union[None,int]=None,
-                             parent_index:Union[Tuple[None,None], Tuple[str,None], Tuple[str,int]]=(None,None)):
-        """Receive a processed_data dictionary and add new key-value pairs to a nested dictionary in processed_data
-        using the json_name and the parent and nested index.
-        Parameters
-        ----------
-        processed_data: dict
-            The dictionary of processed dictionaries, where the key-value pair will be added
-        json_name: str
-            The processed dictionary name
-        key_ref: str
-            The key name to be added
-        value: Any
-            The value to be added on key_ref
-        nested_index: int
-            If the current key-value was inside a list, this value reference the item position in the list
-        parent_index: Tuple[str, int]
-            Reference to the name and index of the parent dictionary, in case the key-value was a 
-            parameter of a dictionary on list in another list.
-        Returns
-        -------
-        processed_data: dict
-        """
-        if nested_index is not None:
-            if parent_index[1] is not None:
-                parent_index_name = f"{parent_index[0]}{self._reference_separator}index"
-                if processed_data[json_name]:
-                    try:
-                        if processed_data[json_name][parent_index[1]]:
-                            try:
-                                processed_data[json_name][parent_index[1]][nested_index][key_ref] = value
-                            except:
-
-                                processed_data[json_name][parent_index[1]].append({parent_index_name:parent_index[1],key_ref:value})
-                        else:
-                            processed_data[json_name][parent_index[1]] = [{parent_index_name:parent_index[1],key_ref:value}]
-                    except Exception as e:
-                        processed_data[json_name].update({parent_index[1]:[{parent_index_name:parent_index[1],key_ref:value}]})
-                else:
-                    processed_data[json_name] = {}
-                    processed_data[json_name].update({parent_index[1]:[{parent_index_name:parent_index[1],key_ref:value}]})
+class CustomFlattener(BaseFlattener):
+    logger.info("CustomFlattener for Python Dictionaries has been initiated")
+
+    def __set__(
+        self,
+        primary_key: Optional[str] = None,
+        entity_name: Optional[str] = None,
+        partition_keys: Optional[list[str]] = None,
+        black_list: Optional[list[str]] = None,
+        case_translator: Optional[CustomCaseTranslator] = None,
+        replace_string: Optional[str] = None,
+    ) -> None:
+        self.primary_key = primary_key if primary_key else self.primary_key
+        self.entity_name = entity_name if entity_name else self.entity_name
+        self.partition_keys = partition_keys if partition_keys else []
+        self.black_list = black_list if black_list is not None else []
+        self.case_translator = (
+            case_translator if case_translator else self.case_translator
+        )
+        self.replace_string = replace_string if replace_string else self.replace_string
+        self.__temp_dict = defaultdict(dict)
+        self.__flatten_dict = defaultdict(list)
+
+    @staticmethod
+    def __split_and_dict(string: str) -> dict[str, str]:
+        pattern = re.compile(r"(\._\d+_)")
+        parts = pattern.split(string)
+        result_dict = {
+            parts[i - 1].lstrip("."): parts[i][2:-1] for i in range(1, len(parts), 2)
+        }
+        return result_dict
+
+    def __set_heritable_fields(self, dictionary: dict[str, Any]) -> None:
+        self._heritable_fields = {
+            dot_join_args(self.entity_name, self.primary_key): dictionary[
+                self.primary_key
+            ]
+        }
+        for partition_key in self.partition_keys:
+            self._heritable_fields[dot_join_args(self.entity_name, partition_key)] = (
+                dictionary[partition_key]
+            )
+
+    def __process_strings(self, string: str) -> str:
+        if self.case_translator is not None:
+            return self.replace_string.join(
+                [
+                    self.case_translator.translate(sub_string)
+                    for sub_string in string.split(".")
+                    if string != ""
+                ]
+            )
+        return self.replace_string.join(string.split("."))
+
+    def __apply_translate(self, dictionary: dict[str, Any]) -> dict[str, Any]:
+        if (self.replace_string != ".") or (self.case_translator is not None):
+            keys = list(dictionary.keys())
+            for key in keys:
+                fixed_key = self.__process_strings(key)
+                dictionary[fixed_key] = dictionary.pop(key)
+        return dictionary
+
+    def __fix_nested_list(self) -> None:
+        dict_names = list(self.__temp_dict.keys())
+        for dict_name in dict_names:
+            index_keys = self.__split_and_dict(dict_name)
+            aux = self.__apply_translate(self.__temp_dict.pop(dict_name))
+            fixed_dict_name = dict_name
+            if index_keys:
+                aux.update(self._heritable_fields)
+                last_key, last_value = index_keys.popitem()
+                trailing_index_key = ""
+                for index_key, index_value in index_keys.items():
+                    trailing_index_key = dot_join_args(trailing_index_key, index_key)
+                    aux[trailing_index_key + self.replace_string + "index"] = int(
+                        index_value
+                    )
+                aux["index"] = int(last_value)
+                fixed_dict_name = dot_join_args(trailing_index_key, last_key)
+            fixed_dict_name = self.__process_strings(fixed_dict_name)
+            if fixed_dict_name in self.__flatten_dict:
+                self.__flatten_dict[fixed_dict_name].append(aux)
             else:
-                try:
-                    processed_data[json_name][nested_index][key_ref] = value
-                except:
-                    if not processed_data[json_name]:
-                        processed_data[json_name] = {}
-                    processed_data[json_name].update({nested_index:{key_ref:value}})
+                self.__flatten_dict[fixed_dict_name] = [aux]
+
+    def __process_list(self, key: str, value: list, dict_name: str, schema_ref: str):
+        if isinstance(value[0], dict):
+            for index, item in enumerate(value):
+                self.__processor(
+                    item,
+                    dot_join_args(dict_name, schema_ref, key, f"_{str(index)}_"),
+                    "",
+                )
         else:
-            try:
-                processed_data[json_name][key_ref] = value
-            except:
-                processed_data[json_name] = [{key_ref:value}]
-        return processed_data
-
-
-    def _processor(self, data:Any, json_name:str, ref:str="", processed_data:dict={}, 
-                   nested_index:Union[None,int]=None,
-                   parent_index:Union[Tuple[None,None], Tuple[str,None], Tuple[str,int]]=(None,None)):
-        """Receive a List or Dictionary data to be flattened, a black list used to skip the keys of a dictionary, a json_name
-        used as reference in case there are a nested list to be flattened in the process.
-        ----------
-        data: Any
-            The dictionary or List to be flattened
-        black_list: List[str]
-            A list of keys to be skipped on flattening process
-        json_name: str
-            The current processed dictionary name
-        Returns
-        -------
-        processed_data: dict
-        """
-        processed_data[json_name] = {} if json_name not in processed_data else processed_data[json_name]
-        if isinstance(data, dict):
-            for key, value in data.items():
-                if key not in self._black_list:
-                    key_name = self._case_translator.translate(key)
-                    key_ref = f"{ref}.{key_name}" if ref!="" else key_name
-                    key_ref = self._replace_dots(key_ref)
-                    if isinstance(value, dict):
-                        self._processor(value, json_name, key_ref, processed_data, nested_index, parent_index)
-                    elif type(value) in (list, tuple):
-                        nested_json_name = self._replace_dots(f"{json_name}.{key_ref}")
-                        self._processor(value, nested_json_name, "", processed_data, nested_index, (json_name, None))
-                    else:
-                        self._insert_child_record(processed_data, json_name, key_ref, value, nested_index, parent_index)
-        elif type(data) in (list, tuple):
-            if nested_index is not None:
-                parent_index = (parent_index[0], nested_index)
-            for index, item in enumerate(data):
-                if isinstance(item, dict):
-                    self._processor(item, json_name, "", processed_data, index, parent_index)
+            for index, item in enumerate(value):
+                dict_item = {key: item}
+                self.__temp_dict[
+                    dot_join_args(dict_name, schema_ref, key, f"_{str(index)}_")
+                ] = dict_item
+
+    def __processor(
+        self, dictionary: dict[str, Any], dict_name: str, schema_ref: str
+    ) -> None:
+        for key, value in dictionary.items():
+            if (
+                not any(
+                    dot_join_args(dict_name, schema_ref, key).endswith(item)
+                    for item in self.black_list
+                )
+                and value is not None
+                and value != []
+                and value != ""
+            ):
+                if isinstance(value, dict):
+                    self.__processor(value, dict_name, dot_join_args(schema_ref, key))
+                elif isinstance(value, list):
+                    self.__process_list(key, value, dict_name, schema_ref)
                 else:
-                    processed_data = self._insert_child_record(processed_data, json_name, "value", item, index, parent_index)
-        else:
-            logger.warning(f"Non-supported data type: {type(data)} with data {data}")
-        return processed_data
-
-
-    def transform(self, json_data, id_key:str, black_list:List[str] = [], json_name:str = "json") -> dict:
-        """Receive a List or Dictionary data to be flattened, a black list used to skip the keys of a dictionary, a json_name
-        used as reference in case there are a nested list to be flattened in the process.
-        ----------
-        json_data: Any
-            The dictionary or List to be flattened
-        id_key: str
-            The 'id' key name, used to add a reference on nested lists
-        black_list: List[str]
-            A list of keys to be skipped on flattening process
-        json_name: str
-            The json name, used to add a reference key-value on nested lists
-        Returns
-        -------
-        processed_data: dict
-        """
-        if not json_data:
-            raise FlatteningException("The provided dictionary is empty.")
-        elif id_key not in json_data:
-            raise FlatteningException(f"The provided id_key={id_key} does not exist in json_data")
-        
-        self._black_list = black_list
-        self._processed_data = {}
-        json_name = self._case_translator.translate(json_name)
-        self._processed_data = self._processor(json_data, json_name, processed_data=self._processed_data)
-        for dict_name, flattened_dicts in self._processed_data.items():
-            if dict_name != json_name:
-                dict_list = []
-                parent_json_id = self._replace_dots(f"{json_name}{self._reference_separator}{id_key}" )
-                for index in flattened_dicts.keys():
-                    if isinstance(flattened_dicts[index], list):
-                        for sub_index, sub_dict in enumerate(flattened_dicts[index]):
-                            sub_dict['index'] = sub_index
-                            sub_dict[parent_json_id] = self._processed_data[json_name][id_key]
-                            dict_list.append(sub_dict)
-                    else:
-                        flattened_dicts[index]['index'] = index
-                        flattened_dicts[index][parent_json_id] = self._processed_data[json_name][id_key]
-                        dict_list.append(flattened_dicts[index])
-                self._processed_data[dict_name] = dict_list
-        return self._processed_data
\ No newline at end of file
+                    self.__temp_dict[dict_name][dot_join_args(schema_ref, key)] = value
+
+    def flatten(
+        self,
+        data: dict[str, Any],
+        primary_key: Optional[str] = None,
+        entity_name: Optional[str] = None,
+        partition_keys: Optional[list[str]] = None,
+        black_list: Optional[list[str]] = None,
+    ) -> dict[str, list[dict[str, Any]]]:
+        self.__set__(primary_key, entity_name, partition_keys, black_list)
+        tmp = data.copy()
+        self.__set_heritable_fields(tmp)
+        tmp[dot_join_args(self.entity_name, self.primary_key)] = data[self.primary_key]
+        for partition_key in self.partition_keys:
+            tmp[dot_join_args(self.entity_name, partition_key)] = data[partition_key]
+        self.__processor(tmp, self.entity_name, "")
+        self.__fix_nested_list()
+        self.__flatten_dict[self.entity_name][0].pop(
+            dot_join_args(self.entity_name, self.primary_key)
+        )
+        for partition_key in self.partition_keys:
+            self.__flatten_dict[self.entity_name][0].pop(
+                dot_join_args(self.entity_name, partition_key)
+            )
+        del tmp
+        return self.__flatten_dict
diff --git a/dataflat/exceptions.py b/dataflat/exceptions.py
index fa7fb2f..1ba795f 100644
--- a/dataflat/exceptions.py
+++ b/dataflat/exceptions.py
@@ -1,11 +1,11 @@
-'''
+"""
 dataflat/exceptions.py - an exception classes script
 
 Copyright (C) 2023 Juan ROJAS
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,9 +15,10 @@
 
 Authors:
     Juan ROJAS <jarojasa97@gmail.com>
-'''
+"""
 from typeguard import typechecked
 
+
 @typechecked
 class FlatteningException(Exception):
     """Exception raised for errors during the dictionary flattening process.
@@ -27,6 +28,6 @@ class FlatteningException(Exception):
         message : str
             Explanation of the error.
     """
-    def __init__(self, message:str):
+    def __init__(self, message: str):
         self.message = message
-        super().__init__(self.message)
\ No newline at end of file
+        super().__init__(self.message)
diff --git a/dataflat/flattener_handler.py b/dataflat/flattener_handler.py
index 4a504f1..991b3ee 100644
--- a/dataflat/flattener_handler.py
+++ b/dataflat/flattener_handler.py
@@ -1,11 +1,11 @@
-'''
+"""
 dataflat/flattener_handler.py - a module handler for dataflat lib
 
-Copyright (C) 2023 Juan ROJAS
+Copyright (C) 2024 Juan ROJAS
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,51 +15,70 @@
 
 Authors:
     Juan ROJAS <jarojasa97@gmail.com>
-'''
+"""
 
 import enum
 from importlib import import_module
-from dataflat.commons import init_logger
+
 from typeguard import typechecked
+
+from dataflat.commons import init_logger
 from dataflat.utils.case_translator import CaseTranslatorOptions, CustomCaseTranslator
 
 logger = init_logger(__name__)
 
+
 class FlattenerOptions(enum.Enum):
     DICTIONARY = 1
-    PANDAS_DF = 2
-    SPARK_DF = 3
+    PYSPARK_DF = 2
+
 
 @typechecked
-class Flattener():
-    def __init__(self):
-        logger.info("Flattener Handler initiated")
+def handler(
+    custom_flattener: FlattenerOptions,
+    from_case: CaseTranslatorOptions = None,
+    to_case: CaseTranslatorOptions = None,
+    replace_string: str = ".",
+    remove_special_chars: bool = False,
+):
+    """Return the selected flattener class from FlattenerOptions
 
-    def handler(self, custom_flattener: FlattenerOptions,
-                from_case: CaseTranslatorOptions=CaseTranslatorOptions.CAMEL_CASE,
-                to_case: CaseTranslatorOptions=CaseTranslatorOptions.SNAKE_CASE,
-                replace_dots:bool=False):
-        """Returns relevant flattener
+    Parameters
+    ----------
+    custom_flattener: FlattenerOptions
+        Specify the Flattener class to use.
+    from_case: CaseTranslatorOptions
+        The original case of the key names in dictionary
+    to_case: CaseTranslatorOptions
+        The destination case of the key names in dictionary
+    replace_string: str
+        String used to separate the nested keys
+    remove_special_chars: bool
+        Remove or not special characters on dataframe or column names
+    Returns
+    -------
+    BaseFlattener -- Flattener class "
+    """
+    flattener = "dataflat.{}.flattener".format(custom_flattener.name.lower())
 
-        Parameters
-        ----------
-        custom_flattener: FlattenerOptions
-            Specify the Flattener class to use.
-        from_case: CaseTranslatorOptions
-            The original case of the key names in dictionary
-        from_case: CaseTranslatorOptions
-            The destination case of the key names in dictionary
-        replace_dots: bool
-            Sepcify if all the dots (used as nested dictionary separator) on processed dictionary
-            will be replaced with underscores
-        Returns
-        -------
-        class -- CustomFlattener object 
-        """
-        split_string = " " if from_case.name in ("CAMEL_CASE","PASCAL_CASE","HUMAN_CASE") else "_" if from_case.name == "SNAKE_CASE" else "-"
-        flattener = "dataflat.{}.flattener".format(custom_flattener.name.lower())
-        if from_case.name == "LOWER_CASE":
-            logger.warning(f"Is not possible to translate from LOWER_CASE to {to_case.name}, switching to LOWER_CASE")
-            to_case = CaseTranslatorOptions.LOWER_CASE
+    if (from_case is None) or (to_case is None):
+        logger.warning(
+            "One or both parameters (from_case,to_case) are None, no translation will be applied."
+        )
+        case_translator = None
+    elif from_case.name == to_case.name:
+        logger.warning(
+            "from_case and to_case are the same, no translation will be applied."
+        )
+        case_translator = None
+    elif from_case.name == "LOWER":
+        logger.warning(
+            f"Is impossible to translate from LOWER to {to_case.name}, no translation will be applied."
+        )
+        case_translator = None
+    else:
+        case_translator = CustomCaseTranslator(from_case, to_case, remove_special_chars)
 
-        return getattr(import_module(flattener), 'CustomFlattener')(CustomCaseTranslator(from_case.name.lower(), to_case.name.lower(), split_string), replace_dots)
\ No newline at end of file
+    return getattr(import_module(flattener), "CustomFlattener")(
+        case_translator=case_translator, replace_string=replace_string
+    )
diff --git a/dataflat/pandas_df/flattener.py b/dataflat/pandas_df/flattener.py
deleted file mode 100644
index f3eca58..0000000
--- a/dataflat/pandas_df/flattener.py
+++ /dev/null
@@ -1,93 +0,0 @@
-'''
-dataflat/pandas_df/flattener.py - The processor script for pandas dataframes flattening process
-
-Copyright (C) 2023 Juan ROJAS
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-Authors:
-    Juan ROJAS <jarojasa97@gmail.com>
-'''
-
-import pandas as pd
-from typeguard import typechecked
-from typing import List
-from dataflat.commons import init_logger
-from dataflat.exceptions import FlatteningException
-from dataflat.dictionary.flattener import CustomFlattener as DictionaryCustomFlattener
-from dataflat.utils.case_translator import CustomCaseTranslator
-
-logger = init_logger(__name__)
-
-@typechecked
-class CustomFlattener():
-    def __init__(self, case_translator:CustomCaseTranslator, replace_dots:bool):
-        logger.info("CustomFlattener for Pandas Dataframes has been initiated")
-        self._case_translator = case_translator
-        self._replace_dots = replace_dots
-
-
-    def transform(self, dataframe:pd.DataFrame, id_key:str, black_list:List[str] = [], dataframe_name:str = "df", chunk_size:int = 500) -> dict:
-        """Receive a pandas Dataframe, and return a dictionary with the
-        flattenend pandas Dataframes.
-        If a black_list is provided then all the column names inside the black_list will
-        be skipped. Notice that if ['name'] is provided as black list, then 
-        all the 'name' columns will be skipped, even if they are under a nested
-        column.
-        The dataframe_name will be used to reference each resulting flattened dataframe
-        under the processed_dataframes dictionary returned.
-
-        Parameters
-        ----------
-        df: pandas.Dataframe
-            The dataframe to be flattened.
-        id_key: str
-            The id key to be used as reference to the parent dataframe.
-        black_list: List[str], (default [])
-            A list of keys to ignore and not to add to the resulting flattened dictionary.
-        dataframe_name: str, (default 'df')
-            A reference name for the dataframe, used to difference each
-            resulting dataframe in the processed_dataframes return.
-        chunk_size: int, (default 500)
-            The chunk size used to process the dataframe in batches. 
-            i.e. If dataframe size is 2000, and chunk_size is 500,
-            then the dataframe will be processed in 4 batches.
-
-        Returns
-        -------
-        processed_dicts : dict
-        """
-        dataframe_len = len(dataframe)
-        if dataframe_len == 0:
-            raise FlatteningException("The provided dataframe is empty.")
-    
-        dict_flattener = DictionaryCustomFlattener(self._case_translator, self._replace_dots)
-        processed_dataframes = {}
-
-        for i in range(0, dataframe_len, chunk_size):
-            records = dataframe[i:i+chunk_size].to_dict('records')
-            processed_dictionaries = {}
-            for dictionary in records:
-                processed_data = dict_flattener.transform(dictionary, id_key, black_list, dataframe_name)
-                for key, value in processed_data.items():
-                    if  key not in processed_dictionaries:
-                        if isinstance(value, list):
-                            processed_dictionaries[key] = value
-                        else:
-                            processed_dictionaries[key] = [value]
-                    else:
-                        if isinstance(value, list):
-                            processed_dictionaries[key].extend(value)
-                        else:
-                            processed_dictionaries[key].extend([value])
-                for key, value in processed_dictionaries.items():
-                    processed_dataframes[key] = pd.DataFrame.from_dict(value)
-        return processed_dataframes
\ No newline at end of file
diff --git a/dataflat/spark_df/__init__.py b/dataflat/pyspark_df/__init__.py
similarity index 100%
rename from dataflat/spark_df/__init__.py
rename to dataflat/pyspark_df/__init__.py
diff --git a/dataflat/pyspark_df/flattener.py b/dataflat/pyspark_df/flattener.py
new file mode 100644
index 0000000..7d2ad29
--- /dev/null
+++ b/dataflat/pyspark_df/flattener.py
@@ -0,0 +1,289 @@
+"""
+dataflat/pyspark/flattener.py - The processor script for spark dataframes flattening process
+
+Copyright (C) 2024 Juan ROJAS
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Authors:
+    Juan ROJAS <jarojasa97@gmail.com>
+"""
+
+import re
+from collections import defaultdict
+from typing import Any, Optional
+
+from pyspark.sql import DataFrame, SparkSession
+from typeguard import typechecked
+
+from dataflat.base.flattener import BaseFlattener
+from dataflat.commons import init_logger
+from dataflat.exceptions import FlatteningException
+from dataflat.utils.case_translator import CustomCaseTranslator
+from dataflat.utils.string import dot_join_args
+
+
+def _add_backticks_if_special_char(string: str):
+    pattern = re.compile(r"[.!@]")
+    string = f"`{string}`" if pattern.search(string) else string
+    return string
+
+
+def _split_field_if_special_char(field: str) -> str:
+    split = field.split(".")
+    field = _add_backticks_if_special_char(split[-1])
+    return ".".join([*split[:-1], field])
+
+
+logger = init_logger(__name__)
+
+
+@typechecked
+class CustomFlattener(BaseFlattener):
+    logger.info("CustomFlattener for PySpark Dataframes has been initiated")
+    spark: SparkSession
+
+    def __set__(
+        self,
+        primary_key: Optional[str],
+        entity_name: Optional[str] = None,
+        partition_keys: Optional[list[str]] = None,
+        black_list: Optional[list[str]] = None,
+        case_translator: Optional[CustomCaseTranslator] = None,
+        replace_string: Optional[str] = None,
+    ):
+
+        self.primary_key = primary_key if primary_key else self.primary_key
+        self.entity_name = entity_name if entity_name else self.entity_name
+        self.partition_keys = partition_keys if partition_keys else []
+        self.black_list = black_list if black_list is not None else []
+        self.case_translator = (
+            case_translator if case_translator else self.case_translator
+        )
+        self.replace_string = replace_string if replace_string else "."
+        self._flattened_schemas: dict[str, list[str]] = {}
+        self._relations: dict[str, str] = {}
+        self._heritable_fields: defaultdict[str, list] = defaultdict(list)
+        self._flattened_dataframes: dict[str, DataFrame] = {}
+
+    def __process_strings(self, string: str):
+        if self.case_translator is not None:
+            return self.replace_string.join(
+                [
+                    self.case_translator.translate(sub_string)
+                    for sub_string in string.split(".")
+                    if string != ""
+                ]
+            )
+        return self.replace_string.join(string.split("."))
+
+    def __apply_column_translate(self):
+        if (self.replace_string != ".") or (self.case_translator is not None):
+            translated_dfs = {}
+            for df_name, df in self._flattened_dataframes.items():
+                select_expr = [
+                    f"`{col}` `{self.__process_strings(col)}`" for col in df.columns
+                ]
+                df = df.selectExpr(*select_expr)
+                fixed_df_name = self.__process_strings(df_name)
+                translated_dfs[fixed_df_name] = df
+            self._flattened_dataframes = translated_dfs
+
+    def __generate_select_query(
+        self, table_name: str, source_table: str, heritable_fields: list[str]
+    ) -> str:
+        def get_columns() -> str:
+            columns = []
+            for field in self._flattened_schemas[table_name]:
+                if "." in field:
+                    columns.append(
+                        f"{_split_field_if_special_char(field)} AS `{field}`"
+                    )
+                else:
+                    columns.append(_add_backticks_if_special_char(field))
+            return ", ".join(columns)
+
+        return (
+            f"SELECT {get_columns()} "
+            f"{',' if heritable_fields and self._flattened_schemas[table_name] else ''}"
+            f"{', '.join(heritable_fields)} "
+            f"FROM {source_table}"
+        )
+
+    def __get_heritable_fields(self, source_table: str) -> list[str]:
+        if source_table not in self._heritable_fields:
+            self._heritable_fields[source_table].extend(
+                [
+                    field
+                    for field in list(self._flattened_dataframes[source_table].columns)
+                    if field.endswith("index")
+                ]
+            )
+        return self._heritable_fields[source_table]
+
+    def __get_nested_struct(
+        self, schema: dict[str, Any], df_name: str, schema_ref: str
+    ) -> None:
+        selected_fields = []
+        fields = [
+            field
+            for field in schema["fields"]
+            if not any(
+                dot_join_args(df_name, schema_ref, field["name"]).endswith(item)
+                for item in self.black_list
+            )
+        ]
+        for field in fields:
+            try:
+                nested_field = field["type"]
+                if nested_field["type"] == "struct":
+                    self.__get_nested_struct(
+                        nested_field, df_name, dot_join_args(schema_ref, field["name"])
+                    )
+                elif nested_field["type"] == "array":
+                    self._relations[
+                        dot_join_args(df_name, schema_ref, field["name"])
+                    ] = df_name
+                    fixed_field_name = dot_join_args("", schema_ref, field["name"])
+                    selected_fields.append(fixed_field_name)
+                    if isinstance(nested_field["elementType"], dict):
+                        self.__get_nested_struct(
+                            nested_field["elementType"],
+                            dot_join_args(df_name, schema_ref, field["name"]),
+                            "",
+                        )
+                    else:
+                        self._flattened_schemas[
+                            dot_join_args(df_name, schema_ref, field["name"])
+                        ] = []
+                else:
+                    raise FlatteningException(
+                        f"{nested_field['type']} is not supported, field {field['name']} will not be processed."
+                    )
+            except (TypeError, FlatteningException):
+                fixed_field_name = dot_join_args("", schema_ref, field["name"])
+                selected_fields.append(fixed_field_name)
+        try:
+            self._flattened_schemas[df_name].extend(selected_fields)
+        except KeyError:
+            self._flattened_schemas.update({df_name: selected_fields})
+
+    def __processor(
+        self,
+        source_table: str,
+        partition_keys: list[str],
+        heritable_fields: list[str],
+        target_table: str,
+        explode_field: str,
+    ) -> list[str]:
+        columns = [
+            f"{partition_key} AS `{self.entity_name}.{partition_key}`"
+            for partition_key in partition_keys
+        ]
+        rename_heritable_fields_query = (
+            f"{self.primary_key} AS `{self.entity_name}.{self.primary_key}`, "
+            f"{', '.join(columns)}"
+        )
+
+        columns = [
+            f"`{self.entity_name}.{partition_key}`" for partition_key in partition_keys
+        ]
+        select_heritable_fields_query = (
+            f"`{self.entity_name}.{self.primary_key}`, {', '.join(columns)}"
+        )
+
+        fields = (
+            rename_heritable_fields_query
+            if source_table == self.entity_name
+            else select_heritable_fields_query
+        )
+        if heritable_fields:
+            columns = [
+                (
+                    f"`{heritable_field}`"
+                    if heritable_field != "index"
+                    else f"{heritable_field} AS `{source_table}.{heritable_field}`"
+                )
+                for heritable_field in heritable_fields
+            ]
+            fields += f", {', '.join(columns)}"
+        explode = True if self._flattened_schemas[target_table] else False
+        exploded_field = explode_field.split(".")[-1]
+        query = (
+            f"SELECT * FROM (SELECT {fields}, POSEXPLODE(`{explode_field}`) "
+            f"AS (index, {exploded_field}) FROM `{source_table}`)"
+        )
+        temp = self.spark.sql(query)
+        heritable_fields = list(temp.columns)
+        if explode:
+            temp = temp.select("*", f"{exploded_field}.*")
+            heritable_fields.remove(exploded_field)
+        temp = temp.drop(exploded_field) if explode else temp
+        temp.createOrReplaceTempView("temp")
+        return heritable_fields
+
+    def flatten(
+        self,
+        data: DataFrame,
+        primary_key: Optional[str] = None,
+        entity_name: Optional[str] = None,
+        partition_keys: Optional[list[str]] = None,
+        black_list: Optional[list[str]] = None,
+    ) -> dict[str, DataFrame]:
+        self.__set__(primary_key, entity_name, partition_keys, black_list)
+        self.spark = SparkSession.getActiveSession()
+        data.createOrReplaceTempView(self.entity_name)
+        self.__get_nested_struct(data.schema.jsonValue(), self.entity_name, "")
+        sorted_dataframes = sorted(
+            list(self._flattened_schemas.keys()), key=lambda k: k.split(".")
+        )
+
+        for table_name in sorted_dataframes:
+            parent_table = (
+                self._relations[table_name]
+                if table_name in self._relations
+                else self.entity_name
+            )
+            explode_col: str = table_name.removeprefix(f"{parent_table}.")
+            source_table = table_name if table_name == self.entity_name else "temp"
+            heritable_fields = []
+
+            if table_name != self.entity_name:
+                source_table = "temp"
+                heritable_fields = self.__processor(
+                    parent_table,
+                    self.partition_keys,
+                    self.__get_heritable_fields(parent_table),
+                    table_name,
+                    explode_col,
+                )
+                heritable_fields = [
+                    f"`{field}`" if "." in field else field
+                    for field in heritable_fields
+                ]
+            select_query = self.__generate_select_query(
+                table_name, source_table, heritable_fields
+            )
+            temp = self.spark.sql(select_query)
+            self._flattened_dataframes[table_name] = self.spark.createDataFrame(
+                data=temp.rdd, schema=temp.schema
+            )
+            self._flattened_dataframes[table_name].createOrReplaceTempView(
+                f"`{table_name}`"
+            )
+            temp.unpersist()
+            if explode_col and table_name != self.entity_name:
+                self._flattened_dataframes[parent_table] = self._flattened_dataframes[
+                    parent_table
+                ].drop(explode_col)
+
+        self.__apply_column_translate()
+        return self._flattened_dataframes
diff --git a/dataflat/requirements.txt b/dataflat/requirements.txt
deleted file mode 100644
index 55cc552..0000000
--- a/dataflat/requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-pandas
-typeguard
\ No newline at end of file
diff --git a/dataflat/spark_df/flattener.py b/dataflat/spark_df/flattener.py
deleted file mode 100644
index 03f6958..0000000
--- a/dataflat/spark_df/flattener.py
+++ /dev/null
@@ -1,187 +0,0 @@
-'''
-dataflat/spark_df/flattener.py - The processor script for spark dataframes flattening process
-
-Copyright (C) 2023 Juan ROJAS
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-Authors:
-    Juan ROJAS <jarojasa97@gmail.com>
-'''
-
-import json
-import re
-from typeguard import typechecked
-from typing import List
-from dataflat.commons import init_logger
-from dataflat.exceptions import FlatteningException
-from dataflat.utils.case_translator import CustomCaseTranslator
-from pyspark.sql.dataframe import DataFrame
-from pyspark.sql.functions import posexplode
-
-logger = init_logger(__name__)
-
-@typechecked
-class CustomFlattener():
-    def __init__(self, case_translator:CustomCaseTranslator, replace_dots:bool):
-        logger.info("CustomFlattener for PySpark Dataframes has been initiated")
-        self._case_translator = case_translator
-        self._reference_separator = "_" if replace_dots else "."
-
-
-    def _process_strings(self, strings:List[str], join_key:str):
-        return join_key.join([self._case_translator.translate(string) for string in strings if string!=""])
-
-
-    def _process_dataframe_columns(self, dataframe_name:str, dataframe:DataFrame):
-        for col in dataframe.columns:
-            renamed_col = re.sub(f"{self._reference_separator}+", self._reference_separator, self._process_strings(col.split("."), self._reference_separator))
-            dataframe = dataframe.withColumnRenamed(col, renamed_col)
-        dataframe_name = re.sub(f"\\{self._reference_separator}\\{self._reference_separator}+", self._reference_separator, self._process_strings(dataframe_name.split("."), self._reference_separator))
-        return (dataframe_name, dataframe)
-
-
-    def _get_schema_struct(self, dataframe_schema:dict, id_key:str, dataframe_name:str, _ref:str = "", named_struct:dict = {}):
-        """Receive Spark Dataframe Schema in dictionary format, and return 
-        a dictionary[str, str] with the all required named_struct expressions
-        to flatten de Dataframe.
-        Parameters
-        ----------
-        dataframe_schema: dict
-            A dictionary with the Dataframe schema.
-        id_key: str
-            The id key to be used as reference to the parent dataframe.
-        black_list: List[str]
-            A list of columns to ignore and not to add to the resulting flattened dictionary.
-        dataframe_name: str
-            A reference name for the dataframe, used to difference each
-            resulting dataframe in the named_struct return.
-        named_struct : dict, default is {}
-        Returns
-        -------
-        named_struct: dict[str, str]
-            A dictionary with all the named_struct expressions to flatten the
-            provided Dataframe schema.
-        """
-        for field in dataframe_schema['fields']:
-            if field['name'] not in self._black_list:
-                field_ref = _ref + f"`{field['name']}`"
-                field_name = field_ref.replace("`","").replace(f"{dataframe_name.split('.')[-1]}.","")
-                if type(field['type']) is dict:
-                    if field['type']['type'] == "array":
-                        field_name_aux = 'value' if isinstance(field['type']['elementType'], str) else field_name
-                        
-                        try:
-                            named_struct[f"{dataframe_name}.{field_name}"] += f"'{self._parent_df_name}_{self._parent_id_key}',{id_key},'{field_name_aux}',{field_ref},"
-                        except:
-                            named_struct[f"{dataframe_name}.{field_name}"] = f"'{self._parent_df_name}_{self._parent_id_key}',{id_key},'{field_name_aux}',{field_ref},"
-                    else:
-                        named_struct = self._get_schema_struct(field['type'], id_key, dataframe_name, f"{field_ref}.", named_struct)
-                elif field['type'] == "map":
-                    logger.warning(f"MapType is not supported, skipping column: {field_name}")
-                else:
-                    try:
-                        named_struct[dataframe_name] += f"'{field_name}',{field_ref},"
-                    except:
-                        named_struct[dataframe_name] = f"'{field_name}',{field_ref},"
-        return named_struct
-
-
-    def _processor(self, dataframe:DataFrame, id_key:str, dataframe_name:str):
-        """Receive a pyspark.Dataframe and returns a Dictionary[str, pyspark.DataFrame]
-        with all the processed dataframes.
-        Parameters
-        ----------
-        dataframe: pyspark.DataFrame
-            A Dataframe to be flattened
-        id_key: str
-            The id key to be used as reference to the parent dataframe.
-        black_list: List[str]
-            A list of columns to ignore and not to add to the resulting flattened dictionary.
-        dataframe_name: str
-            A reference name for the dataframe, used to difference each
-            resulting dataframe in the named_struct return.
-        Returns
-        -------
-        processed_data : dict[str, pyspark.DataFrame]
-            A dictionary with all the resulting Dataframes from the flattening process.
-            Each ArrayType column will be flattened and added as a Dataframe inside the
-            processed_data return.
-        """
-        processed_data = {}
-        schema = json.loads(dataframe.schema.json())
-        ns = self._get_schema_struct(schema, id_key, dataframe_name, _ref="", named_struct={})
-        parent_pos = f",'{dataframe_name}_index',`index`" if "'index',`index`" in ns[dataframe_name] else ""
-        for df_name, struct in ns.items():
-            if df_name != dataframe_name:
-                selectExpr = f"named_struct( {struct[:-1]}{parent_pos} ) as Item"
-                aux_df = dataframe.selectExpr( selectExpr ).select("Item.*")
-                child_id_key = aux_df.columns[0] if "." not in aux_df.columns[0] else f"`{aux_df.columns[0]}`"
-                expld_col = aux_df.columns[1]
-                expld_expr = expld_col if "." not in expld_col else f"`{expld_col}`"
-                selected_cols = [child_id_key, posexplode(expld_expr).alias('index', expld_col)]
-                if parent_pos:
-                    selected_cols.append(f'`{dataframe_name}_index`')
-                aux_df = aux_df.select(selected_cols) \
-                    .withColumnRenamed(expld_col, expld_col.split(".")[-1])
-                processed_data[df_name] = aux_df
-                for types in aux_df.dtypes:
-                    if "struct<" in types[1]:
-                        nested_dfs = self._processor(aux_df, child_id_key, df_name)
-                        processed_data.update(nested_dfs)
-            else:
-                selectExpr = f"named_struct( {struct[:-1]} ) as Item"
-                aux_df = dataframe.selectExpr( selectExpr ).select("Item.*")
-                processed_data[df_name] = aux_df
-        return processed_data
-
-
-    def transform(self, dataframe:DataFrame, id_key:str, black_list:List[str] = [], dataframe_name:str = "df"):
-        """Receive a pyspark.Dataframe and returns a Dictionary[str, pyspark.DataFrame]
-        with all the processed dataframes.
-        The ammount of dataframes on returned dictionary will depends on the number
-        of ArrayType columns present on the input dataframe.
-        If to_snake_case is set True, the column names on every processed Dataframe
-        will be casted to snake_case.
-        If replace_docts is set to True, all the dots present on a column name will
-        be replaces such as follow example, "Column.SubColumn" -> "Column_SubColumn".
-        Parameters
-        ----------
-        dataframe: pyspark.DataFrame
-            A Dataframe to be flattened
-        id_key: str
-            The id key to be used as reference to the parent dataframe.
-        black_list: List[str]
-            A list of columns to ignore and not to add to the resulting flattened dictionary.
-        dataframe_name: str
-            A reference name for the dataframe, used to difference each
-            resulting dataframe in the named_struct return.
-        Returns
-        -------
-        processed_data : dict[str, pyspark.DataFrame]
-            A dictionary with all the resulting Dataframes from the flattening process.
-            Each ArrayType column will be flattened and added as a Dataframe inside the
-            processed_data return.
-        """
-        if len(dataframe.head(1)) == 0:
-            raise FlatteningException("The provided dataframe is empty.")
-        elif id_key not in dataframe.columns:
-            raise FlatteningException(f"The provided id_key={id_key} does not exist in dataframe")
-        
-        self._parent_df_name = dataframe_name
-        self._parent_id_key = id_key
-        self._black_list = black_list
-        aux, self._processed_data = {}, {}
-        aux = self._processor(dataframe, id_key, dataframe_name)
-        for processed_df_name, processed_df in aux.items():
-            processed_aux = self._process_dataframe_columns(processed_df_name, processed_df)
-            self._processed_data[processed_aux[0]] = processed_aux[1]
-        return self._processed_data
\ No newline at end of file
diff --git a/dataflat/utils/case_translator.py b/dataflat/utils/case_translator.py
index d42ca4b..eb520e1 100644
--- a/dataflat/utils/case_translator.py
+++ b/dataflat/utils/case_translator.py
@@ -1,11 +1,11 @@
-'''
+"""
 dataflat/utils/case_translator.py - The case translator script for strings
 
-Copyright (C) 2023 Juan ROJAS
+Copyright (C) 2024 Juan ROJAS
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
+    https://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,35 +15,39 @@
 
 Authors:
     Juan ROJAS <jarojasa97@gmail.com>
-'''
+"""
 
 import enum
 import re
-from dataflat.commons import init_logger
-from typeguard import typechecked
 from typing import List
 
+from typeguard import typechecked
+
+from dataflat.commons import init_logger
+
 logger = init_logger(__name__)
 
-class CaseTranslatorOptions(enum.Enum): # Possible values
-    SNAKE_CASE = 1
-    KEBAB_CASE = 2
-    CAMEL_CASE = 3
-    PASCAL_CASE = 4
-    LOWER_CASE = 5
-    HUMAN_READABLE = 6
+
+class CaseTranslatorOptions(enum.Enum):     # Possible values
+    SNAKE = {"id": 1, "split_string": "_"}
+    KEBAB = {"id": 2, "split_string": "-"}
+    CAMEL = {"id": 3, "split_string": " "}
+    PASCAL = {"id": 4, "split_string": " "}
+    HUMAN = {"id": 5, "split_string": " "}
+    LOWER = {"id": 6, "split_string": " "}
+
 
 @typechecked
-class CustomCaseTranslator():
-    def __init__(self, from_case:str, to_case:str, split_string:str=""):
-        logger.info(f"CustomCaseTranslator for {from_case} to {to_case} has been initiated")
+class CustomCaseTranslator:
+    def __init__(self, from_case: CaseTranslatorOptions, to_case: CaseTranslatorOptions, remove_special_chars: bool):
+        logger.info(f"CustomCaseTranslator for {from_case.name} to {to_case.name} has been initiated")
         self.from_case = from_case
         self.to_case = to_case
-        self.split_string = split_string
+        self.remove_special_chars = remove_special_chars
 
-    def _pre_process_string(self, string:str) -> str:
-        """Receive an input string in camel or pascal case and process it
-        to return a character splitted string.
+    def _pre_process_string(self, string: str) -> str:
+        """Receive an input string in camel or Pascal case and process it
+        to return a split string.
 
         Parameters
         ----------
@@ -53,14 +57,25 @@ def _pre_process_string(self, string:str) -> str:
         -------
         conv_string: str
         """
-        conv_string = re.sub('(.)([A-Z][a-z]+)', r'\1 \2', re.sub(r'\W+', "", string))
-        conv_string = re.sub('(.)([0-9]+)', r'\1 \2', conv_string)
-        conv_string = re.sub('([a-z0-9])([A-Z])', r'\1 \2', conv_string)
+        # Step 1: Handle camel case, Pascal case, and mixed alphanumeric patterns
+        conv_string = re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)  # camelCase to camel Case
+        conv_string = re.sub(r'(?<=[A-Z])(?=[A-Z][a-z])', ' ', conv_string)  # PascalCase to Pascal Case
+        conv_string = re.sub(r'(?<=[a-z])(?=\d)', ' ', conv_string)  # text123 to text 123
+        conv_string = re.sub(r'(?<=\d)(?=[a-zA-Z])', ' ', conv_string)  # 123text to 123 text
+        conv_string = re.sub(r'(?<=\d)(?=[A-Z])', ' ', conv_string)  # 123ABC to 123 ABC
+
+        # Handle special characters if `remove_special_chars` is False
+        if not self.remove_special_chars:
+            # Ensure spaces around special characters
+            conv_string = re.sub(r'([^\w\s])', r' \1 ', conv_string)
+
+        # Remove extra spaces around special characters and condense multiple spaces
+        conv_string = re.sub(r'\s+', ' ', conv_string).strip()
         return conv_string
 
-    def _normalize(self, string:str) -> List[str]:
-        """Receive a splittable string, removes all non alphanumerical or underscore
-        characters, and return a list of the proccesed words in the string
+    def _normalize(self, string: str) -> List[str]:
+        """Receive a splittable string, removes all non-alphanumerical or underscore
+        characters, and return a list of the processed words in the string
 
         Parameters
         ----------
@@ -70,10 +85,10 @@ def _normalize(self, string:str) -> List[str]:
         -------
         string: str
         """
-        replaced_strings = [re.sub(r'\W+', "", sub_string) for sub_string in string.split(self.split_string)]
-        return [replaced_string.lower() for replaced_string in replaced_strings if replaced_string!=""]
+        replaced_strings = string.split(self.from_case.value['split_string'])
+        return [replaced_string.lower() for replaced_string in replaced_strings if replaced_string != ""]
 
-    def _kebab_case(self, string:str) -> str:
+    def _kebab(self, string: str) -> str:
         """Receive a string and convert it to kebab-case
 
         Parameters
@@ -86,7 +101,7 @@ def _kebab_case(self, string:str) -> str:
         """
         return "-".join(self._normalize(string))
 
-    def _snake_case(self, string:str) -> str:
+    def _snake(self, string: str) -> str:
         """Receive a string and convert it to snake_case
 
         Parameters
@@ -99,7 +114,7 @@ def _snake_case(self, string:str) -> str:
         """
         return "_".join(self._normalize(string))
 
-    def _camel_case(self, string:str) -> str:
+    def _camel(self, string: str) -> str:
         """Receive a string and convert it to camelCase
 
         Parameters
@@ -110,9 +125,9 @@ def _camel_case(self, string:str) -> str:
         -------
         string: str
         """
-        return "".join([word.capitalize() if index>0 else word for index, word in enumerate(self._normalize(string))])
+        return "".join([word.capitalize() if index > 0 else word for index, word in enumerate(self._normalize(string))])
 
-    def _pascal_case(self, string:str) -> str:
+    def _pascal(self, string: str) -> str:
         """Receive a string and convert it to PascalCase
 
         Parameters
@@ -125,7 +140,7 @@ def _pascal_case(self, string:str) -> str:
         """
         return "".join([word.capitalize() for word in self._normalize(string)])
     
-    def _lower_case (self, string:str) -> str:
+    def _lower(self, string: str) -> str:
         """Receive a string and convert it to lowercase
 
         Parameters
@@ -138,7 +153,7 @@ def _lower_case (self, string:str) -> str:
         """
         return "".join(self._normalize(string)).lower()
 
-    def _human_readable (self, string:str) -> str:
+    def _human(self, string: str) -> str:
         """Receive a string and convert it to Human readable
 
         Parameters
@@ -149,9 +164,11 @@ def _human_readable (self, string:str) -> str:
         -------
         string: str
         """
-        return " ".join([word if index>0 else word.capitalize() for index, word in enumerate(self._normalize(string))])
+        return " ".join(
+            [word if index > 0 else word.capitalize() for index, word in enumerate(self._normalize(string))]
+        )
     
-    def translate(self, string:str) -> str:
+    def translate(self, string: str) -> str:
         """Receive a string and convert it to the
         desirable case.
 
@@ -163,6 +180,8 @@ def translate(self, string:str) -> str:
         -------
         string: str
         """
-        if self.from_case in ('camel_case','pascal_case'):
+        if self.remove_special_chars:
+            string = re.sub(r'\W+', "", string)
+        if self.from_case.name in ('CAMEL', 'PASCAL'):
             string = self._pre_process_string(string)
-        return getattr(self, f"_{self.to_case}")(string)
\ No newline at end of file
+        return getattr(self, f"_{self.to_case.name.lower()}")(string)
diff --git a/dataflat/utils/string.py b/dataflat/utils/string.py
new file mode 100644
index 0000000..197afc0
--- /dev/null
+++ b/dataflat/utils/string.py
@@ -0,0 +1,2 @@
+def dot_join_args(*argv) -> str:
+    return '.'.join([arg for arg in argv if arg])
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..ff2a167
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,59 @@
+[project]
+name = "dataflat"
+version = "2.0.0"
+description = "A library to flatten nested data."
+authors = [
+  {name = "Juan Rojas", email = "jarojasa97@gmail.com"},
+]
+requires-python = ">= 3.10"
+dependencies = ["typeguard==4.3.0"]
+keywords = ["flattening", "nested", "data", "pyspark"]
+classifiers=[
+    'Development Status :: 5 - Production/Stable',
+    'Intended Audience :: Developers',
+    'Intended Audience :: Information Technology',
+    'Intended Audience :: Science/Research',
+    'License :: OSI Approved :: Apache Software License',
+    'Natural Language :: English',
+    'Operating System :: OS Independent',
+    'Programming Language :: Python :: 3',
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    'Topic :: Scientific/Engineering',
+    'Topic :: Software Development :: Libraries',
+    'Topic :: Software Development :: Libraries :: Python Modules',
+    'Topic :: Software Development :: Pre-processors',
+    'Topic :: Utilities'
+]
+readme = "README.md"
+license = {file = "LICENSE"}
+
+[project.urls]
+Repository = "https://github.com/JuanARojasA/dataflat.git"
+Issues = "https://github.com/JuanARojasA/dataflat/issues"
+Changelog = "https://github.com/JuanARojasA/dataflat/blob/master/CHANGELOG.md"
+
+[project.optional-dependencies]
+test = [
+    "coverage==7.6.1",
+    "pytest==8.3.2",
+    "numpy==1.26.4",
+    "pandas==2.0.3",
+    "pyspark==3.5.0",
+    "testcontainers==4.8.0"
+]
+
+[tool.commitizen]
+name = "cz_conventional_commits"
+tag_format = "v$version"
+version_scheme = "pep440"
+version_provider = "pep621"
+update_changelog_on_bump = true
+bump_message = "bump: version $current_version → $new_version"
+annotated_tag = true
+
+[tool.pytest.ini_options]
+markers = [
+    "slow: marks tests as slow (deselect with '-m \"not slow\"')"
+]
\ No newline at end of file
diff --git a/setup.py b/setup.py
deleted file mode 100644
index c95d7f4..0000000
--- a/setup.py
+++ /dev/null
@@ -1,58 +0,0 @@
-'''
-setup.py - a setup script
-
-Copyright (C) 2023 Juan ROJAS
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-Authors:
-    Juan ROJAS <jarojasa97@gmail.com>
-'''
-
-import setuptools
-from codecs import open
-from os import path
-
-PATH = path.abspath(path.dirname(__file__))
-
-with open(path.join(PATH, 'README.md'), encoding='utf-8') as f:
-    long_description = f.read()
- 
-setuptools.setup(
-    name="dataflat",
-    version="1.1.0",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url='https://github.com/JuanARojasA/dataflat',
-    author="Juan Rojas",
-    author_email="jarojasa97@gmail.com",
-    license='Apache License 2.0',
-    platforms='any',
-    packages=setuptools.find_packages(),
-    classifiers=[
-            'Development Status :: 5 - Production/Stable',
-            'Intended Audience :: Developers',
-            'Intended Audience :: Information Technology',
-            'Intended Audience :: Science/Research',
-            'License :: OSI Approved :: Apache Software License',
-            'Natural Language :: English',
-            'Operating System :: OS Independent',
-            'Programming Language :: Python :: 3',
-            'Topic :: Scientific/Engineering',
-            'Topic :: Software Development :: Libraries',
-            'Topic :: Software Development :: Libraries :: Python Modules',
-            'Topic :: Software Development :: Pre-processors',
-            'Topic :: Utilities'
-        ],
-    python_requires='>=3.9',
-    install_requires=["pandas","typeguard"],
-    include_package_data=True
-)
\ No newline at end of file
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..6b0dace
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,9 @@
+sonar.projectKey=JuanARojasA_dataflat
+sonar.organization=jrojas
+
+sonar.projectName=dataflat
+sonar.projectVersion=2.0
+sonar.test=tests
+sonar.python.coverage.reportPaths=coverage-report/coverage.xml
+sonar.coverage.exclusions=tests/**
+sonar.exclusions=tests/**
\ No newline at end of file


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/base_test/__init__.py b/tests/base_test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/base_test/flattener_test.py b/tests/base_test/flattener_test.py
new file mode 100644
index 0000000..45a3c5f
--- /dev/null
+++ b/tests/base_test/flattener_test.py
@@ -0,0 +1,43 @@
+from dataflat.base.flattener import BaseFlattener
+from dataflat.utils.case_translator import CaseTranslatorOptions
+
+
+def test_flattener():
+    base = BaseFlattener()
+    assert base.case_translator is None
+    assert base.replace_string == "."
+    assert base.entity_name == "data"
+    assert base.primary_key == "id"
+
+
+def test_flattener_with_case_translator(get_custom_flattener):
+    from_case = CaseTranslatorOptions.CAMEL
+    to_case = CaseTranslatorOptions.SNAKE
+    base: BaseFlattener = get_custom_flattener(BaseFlattener, from_case, to_case)
+    assert base.case_translator is not None
+    assert base.case_translator.from_case == from_case
+    assert base.case_translator.to_case == to_case
+
+
+def test_flattener_with_parameters():
+    base = BaseFlattener(replace_string=",")
+    assert base.replace_string == ","
+
+
+def test_flattener_with_entity_name():
+    entity_name = "tests"
+    base = BaseFlattener(entity_name=entity_name)
+    assert base.entity_name == entity_name
+
+
+def test_flattener_with_primary_key():
+    primary_key = "tests"
+    base = BaseFlattener(primary_key=primary_key)
+    assert base.primary_key == primary_key
+
+
+def test_flatten_abstract(get_custom_flattener):
+    from_case = CaseTranslatorOptions.CAMEL
+    to_case = CaseTranslatorOptions.SNAKE
+    base: BaseFlattener = get_custom_flattener(BaseFlattener, from_case, to_case)
+    assert base.flatten({}) is None
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..21b5f1d
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,70 @@
+import hashlib
+import os
+
+from pyspark.sql import SparkSession
+from pytest import fixture
+from testcontainers.core.container import DockerContainer
+
+from dataflat.utils.case_translator import CaseTranslatorOptions, CustomCaseTranslator
+
+
+@fixture(scope="function")
+def get_custom_flattener():
+    def _get_custom_flattener(
+        flattener_class,
+        from_case: CaseTranslatorOptions,
+        to_case: CaseTranslatorOptions,
+    ):
+        case_translator = CustomCaseTranslator(
+            from_case=from_case,
+            to_case=to_case,
+            remove_special_chars=False,
+        )
+        return flattener_class(case_translator, replace_string=".")
+
+    return _get_custom_flattener
+
+
+@fixture(scope="function")
+def get_full_path():
+    def _get_full_path(case: str, entity: str) -> str:
+        file_path = os.path.join(
+            os.path.dirname(__file__), "resources", case, f"{entity}.json"
+        )
+        return file_path
+
+    return _get_full_path
+
+
+@fixture(scope="function")
+def compare_result():
+    def _compare_result(result: str, expected_result_filepath: str):
+        result_md5 = hashlib.sha256(result.encode("utf-8")).hexdigest()
+        with open(expected_result_filepath, "rb") as f:
+            expected_result_md5 = hashlib.sha256(f.read()).hexdigest()
+        return result_md5 == expected_result_md5
+
+    return _compare_result
+
+
+@fixture(scope="function")
+def ignore_null_keys():
+    def _ignore_null_keys(d):
+        return {k: v for k, v in d.items() if v is not None}
+
+    return _ignore_null_keys
+
+
+@fixture(scope="session")
+def spark():
+    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")
+    spark_container = (
+        DockerContainer("bitnami/spark:3.5.0")
+        .with_exposed_ports(8080, 7077)
+        .with_volume_mapping(path, path)
+    )
+    with spark_container:
+        spark = (
+            SparkSession.builder.appName("TestClient").master("local[*]").getOrCreate()
+        )
+        yield spark
diff --git a/tests/dicitonary_test/__init__.py b/tests/dicitonary_test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/dicitonary_test/flattener_test.py b/tests/dicitonary_test/flattener_test.py
new file mode 100644
index 0000000..94d2e67
--- /dev/null
+++ b/tests/dicitonary_test/flattener_test.py
@@ -0,0 +1,73 @@
+import json
+from collections import OrderedDict
+
+from dataflat.dictionary.flattener import CustomFlattener
+from dataflat.utils.case_translator import CaseTranslatorOptions
+
+
+def test_flattener():
+    base = CustomFlattener()
+    assert base.case_translator is None
+    assert base.replace_string == "."
+    assert base.entity_name == "data"
+    assert base.primary_key == "id"
+
+
+def test_flatten_camel_to_snake(get_custom_flattener, get_full_path, compare_result):
+    from_case = CaseTranslatorOptions.CAMEL
+    to_case = CaseTranslatorOptions.SNAKE
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    with open(get_full_path(from_case.name.lower(), "original")) as f:
+        data = json.load(f)
+    results = flattener.flatten(data, partition_keys=["date"])
+
+    for entity, result in results.items():
+        string_result = "\n".join(
+            json.dumps(OrderedDict(sorted(item.items())), separators=(",", ":"))
+            for item in result
+        )
+        assert compare_result(
+            string_result, get_full_path(to_case.name.lower(), entity)
+        )
+
+
+def test_flatten_snake_to_camel(get_custom_flattener, get_full_path, compare_result):
+    from_case = CaseTranslatorOptions.SNAKE
+    to_case = CaseTranslatorOptions.CAMEL
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    with open(get_full_path(from_case.name.lower(), "original")) as f:
+        data = json.load(f)
+    results = flattener.flatten(data, partition_keys=["date"])
+
+    for entity, result in results.items():
+        string_result = "\n".join(
+            json.dumps(OrderedDict(sorted(item.items())), separators=(",", ":"))
+            for item in result
+        )
+        assert compare_result(
+            string_result, get_full_path(to_case.name.lower(), entity)
+        )
+
+
+def test_flatten_black_list(get_custom_flattener, get_full_path, compare_result):
+    from_case = CaseTranslatorOptions.SNAKE
+    to_case = CaseTranslatorOptions.SNAKE
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    with open(get_full_path("black_list", "original")) as f:
+        data = json.load(f)
+    results = flattener.flatten(
+        data, black_list=["total_orders", "summary.total_clients"]
+    )
+
+    for entity, result in results.items():
+        string_result = "\n".join(
+            json.dumps(OrderedDict(sorted(item.items())), separators=(",", ":"))
+            for item in result
+        )
+        assert compare_result(string_result, get_full_path("black_list", entity))
diff --git a/tests/pyspark_df_test/__init__.py b/tests/pyspark_df_test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/pyspark_df_test/flattener_test.py b/tests/pyspark_df_test/flattener_test.py
new file mode 100644
index 0000000..499c122
--- /dev/null
+++ b/tests/pyspark_df_test/flattener_test.py
@@ -0,0 +1,104 @@
+import json
+
+import pytest
+from pyspark.sql import SparkSession
+
+from dataflat.pyspark_df.flattener import CustomFlattener
+from dataflat.utils.case_translator import CaseTranslatorOptions
+
+
+@pytest.mark.slow
+def test_flattener():
+    base = CustomFlattener()
+    assert base.case_translator is None
+    assert base.replace_string == "."
+    assert base.entity_name == "data"
+    assert base.primary_key == "id"
+
+
+@pytest.mark.slow
+def test_flatten_camel_to_snake(
+    spark: SparkSession,
+    get_custom_flattener,
+    get_full_path,
+    compare_result,
+    ignore_null_keys,
+):
+    from_case = CaseTranslatorOptions.CAMEL
+    to_case = CaseTranslatorOptions.SNAKE
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    data = spark.read.json(get_full_path(from_case.name.lower(), "original"))
+    results = flattener.flatten(data, partition_keys=["date"])
+
+    for entity, result in results.items():
+        sorted_columns = [f"`{col}`" for col in sorted(result.columns)]
+        string_result = (
+            result.select(sorted_columns).toPandas().to_json(orient="records")
+        )
+        filtered_json = "\n".join(
+            [
+                json.dumps(ignore_null_keys(record), separators=(",", ":"))
+                for record in json.loads(string_result)
+            ]
+        )
+        assert compare_result(
+            filtered_json, get_full_path(to_case.name.lower(), entity)
+        )
+
+
+@pytest.mark.slow
+def test_flatten_snake_to_camel(
+    spark, get_custom_flattener, get_full_path, compare_result, ignore_null_keys
+):
+    from_case = CaseTranslatorOptions.SNAKE
+    to_case = CaseTranslatorOptions.CAMEL
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    data = spark.read.json(get_full_path(from_case.name.lower(), "original"))
+    results = flattener.flatten(data, partition_keys=["date"])
+
+    for entity, result in results.items():
+        sorted_columns = [f"`{col}`" for col in sorted(result.columns)]
+        string_result = (
+            result.select(sorted_columns).toPandas().to_json(orient="records")
+        )
+        filtered_json = "\n".join(
+            [
+                json.dumps(ignore_null_keys(record), separators=(",", ":"))
+                for record in json.loads(string_result)
+            ]
+        )
+        assert compare_result(
+            filtered_json, get_full_path(to_case.name.lower(), entity)
+        )
+
+
+@pytest.mark.slow
+def test_flatten_black_list(
+    spark, get_custom_flattener, get_full_path, compare_result, ignore_null_keys
+):
+    from_case = CaseTranslatorOptions.SNAKE
+    to_case = CaseTranslatorOptions.SNAKE
+    flattener: CustomFlattener = get_custom_flattener(
+        CustomFlattener, from_case, to_case
+    )
+    data = spark.read.json(get_full_path("black_list", "original"))
+    results = flattener.flatten(
+        data, black_list=["total_orders", "summary.total_clients"]
+    )
+
+    for entity, result in results.items():
+        sorted_columns = [f"`{col}`" for col in sorted(result.columns)]
+        string_result = (
+            result.select(sorted_columns).toPandas().to_json(orient="records")
+        )
+        filtered_json = "\n".join(
+            [
+                json.dumps(ignore_null_keys(record), separators=(",", ":"))
+                for record in json.loads(string_result)
+            ]
+        )
+        assert compare_result(filtered_json, get_full_path("black_list", entity))
diff --git a/tests/resources/black_list/data.json b/tests/resources/black_list/data.json
new file mode 100644
index 0000000..524a140
--- /dev/null
+++ b/tests/resources/black_list/data.json
@@ -0,0 +1,1 @@
+{"date":"2024-09-03","id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","summary.most_recent_order_date":"2024-09-02","summary.total_revenue":2145.98}
\ No newline at end of file
diff --git a/tests/resources/black_list/original.json b/tests/resources/black_list/original.json
new file mode 100644
index 0000000..29c4ef9
--- /dev/null
+++ b/tests/resources/black_list/original.json
@@ -0,0 +1,1 @@
+{"id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-09-03","total_orders":2,"summary":{"total_revenue":2145.98,"total_clients":2,"most_recent_order_date":"2024-09-02"}}
\ No newline at end of file
diff --git a/tests/resources/camel/data.json b/tests/resources/camel/data.json
new file mode 100644
index 0000000..c89263f
--- /dev/null
+++ b/tests/resources/camel/data.json
@@ -0,0 +1,1 @@
+{"date":"2024-09-03","id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","summary.mostRecentOrderDate":"2024-09-02","summary.totalClients":2,"summary.totalRevenue":2145.98,"totalOrders":2}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.client.addresses.json b/tests/resources/camel/data.orders.client.addresses.json
new file mode 100644
index 0000000..8ded2a0
--- /dev/null
+++ b/tests/resources/camel/data.orders.client.addresses.json
@@ -0,0 +1,4 @@
+{"address":"123 Billing St, Springfield, IL","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"type":"billing"}
+{"address":"456 Shipping Ave, Springfield, IL","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"type":"shipping"}
+{"address":"789 Billing St, Metropolis, NY","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"type":"billing"}
+{"address":"101 Shipping Blvd, Metropolis, NY","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"type":"shipping"}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.items.attributes.json b/tests/resources/camel/data.orders.items.attributes.json
new file mode 100644
index 0000000..f1864f0
--- /dev/null
+++ b/tests/resources/camel/data.orders.items.attributes.json
@@ -0,0 +1,8 @@
+{"attributeName":"Color","attributeValue":"Silver","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":0}
+{"attributeName":"Warranty","attributeValue":"2 years","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":1}
+{"attributeName":"Color","attributeValue":"Black","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":0}
+{"attributeName":"Connectivity","attributeValue":"Bluetooth","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":1}
+{"attributeName":"Color","attributeValue":"Gold","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":0}
+{"attributeName":"Storage","attributeValue":"128GB","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":1}
+{"attributeName":"Material","attributeValue":"Silicone","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":0}
+{"attributeName":"Color","attributeValue":"Blue","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":1}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.items.json b/tests/resources/camel/data.orders.items.json
new file mode 100644
index 0000000..50ea801
--- /dev/null
+++ b/tests/resources/camel/data.orders.items.json
@@ -0,0 +1,4 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"itemId":"a7f4c3e2-df5c-4ed1-9119-9f42cba18fb9","name":"Laptop","price":"1200","quantity":1}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"itemId":"b9f8d7e9-2b60-4f1b-92d2-f8d99aefa9b3","name":"Wireless Mouse","price":"25.5","quantity":2}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"itemId":"c0d3e4f6-b2ab-4d67-83e6-1e89c9e8a9c7","name":"Smartphone","price":"899.99","quantity":1}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"itemId":"d1f7g8h9-2b74-4c9e-92f1-a8b9d9a8b9a2","name":"Phone Case","price":"19.99","quantity":3}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.items.tags.json b/tests/resources/camel/data.orders.items.tags.json
new file mode 100644
index 0000000..0b2423e
--- /dev/null
+++ b/tests/resources/camel/data.orders.items.tags.json
@@ -0,0 +1,12 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":0,"tags":"Electronics"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":1,"tags":"Portable"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":2,"tags":"High-End"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":0,"tags":"Accessories"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":1,"tags":"Wireless"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":2,"tags":"Peripherals"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":0,"tags":"Electronics"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":1,"tags":"Mobile"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":2,"tags":"Gadgets"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":0,"tags":"Accessories"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":1,"tags":"Protective"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":2,"tags":"Case"}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.json b/tests/resources/camel/data.orders.json
new file mode 100644
index 0000000..74b7406
--- /dev/null
+++ b/tests/resources/camel/data.orders.json
@@ -0,0 +1,2 @@
+{"client.clientId":"5e8b25d6-7f97-4d79-a6e0-55e0e7085a62","client.contactInfo.email":"johndoe@example.com","client.contactInfo.phone":"+123456789","client.name":"John Doe","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-09-02","index":0,"orderId":"d6cbe4a4-b563-4c69-8473-e0f8a6636d3a","payment.cardLastFour":"1234","payment.method":"Credit Card","payment.status":"Paid","payment.transactionId":"78a0caae-b1f4-4ebf-90e4-2a1d8f9a63f1"}
+{"client.clientId":"cd8e9d76-45d2-4f32-85c9-1e68b2a8a4e7","client.contactInfo.email":"janesmith@example.com","client.contactInfo.phone":"+987654321","client.name":"Jane Smith","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-08-29","index":1,"orderId":"ab5e35c1-f1a2-4bb4-badf-9f1b6f9a6b72","payment.method":"PayPal","payment.status":"Pending","payment.transactionId":"43b2e7ea-b6b4-4f7e-b1c9-8a9d8f1b7f6c"}
\ No newline at end of file
diff --git a/tests/resources/camel/data.orders.notes.json b/tests/resources/camel/data.orders.notes.json
new file mode 100644
index 0000000..844f17c
--- /dev/null
+++ b/tests/resources/camel/data.orders.notes.json
@@ -0,0 +1,4 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"notes":"Order processed on priority"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"notes":"Customer requested gift wrapping"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"notes":"Awaiting payment confirmation"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"notes":"Customer preferred express shipping"}
\ No newline at end of file
diff --git a/tests/resources/camel/original.json b/tests/resources/camel/original.json
new file mode 100644
index 0000000..fa5550e
--- /dev/null
+++ b/tests/resources/camel/original.json
@@ -0,0 +1,1 @@
+{"id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-09-03","orders":[{"orderId":"d6cbe4a4-b563-4c69-8473-e0f8a6636d3a","date":"2024-09-02","client":{"clientId":"5e8b25d6-7f97-4d79-a6e0-55e0e7085a62","name":"John Doe","contactInfo":{"email":"johndoe@example.com","phone":"+123456789"},"addresses":[{"type":"billing","address":"123 Billing St, Springfield, IL"},{"type":"shipping","address":"456 Shipping Ave, Springfield, IL"}]},"items":[{"itemId":"a7f4c3e2-df5c-4ed1-9119-9f42cba18fb9","name":"Laptop","quantity":1,"price":"1200","attributes":[{"attributeName":"Color","attributeValue":"Silver"},{"attributeName":"Warranty","attributeValue":"2 years"}],"tags":["Electronics","Portable","High-End"]},{"itemId":"b9f8d7e9-2b60-4f1b-92d2-f8d99aefa9b3","name":"Wireless Mouse","quantity":2,"price":"25.5","attributes":[{"attributeName":"Color","attributeValue":"Black"},{"attributeName":"Connectivity","attributeValue":"Bluetooth"}],"tags":["Accessories","Wireless","Peripherals"]}],"payment":{"method":"Credit Card","cardLastFour":"1234","status":"Paid","transactionId":"78a0caae-b1f4-4ebf-90e4-2a1d8f9a63f1"},"notes":["Order processed on priority","Customer requested gift wrapping"]},{"orderId":"ab5e35c1-f1a2-4bb4-badf-9f1b6f9a6b72","date":"2024-08-29","client":{"clientId":"cd8e9d76-45d2-4f32-85c9-1e68b2a8a4e7","name":"Jane Smith","contactInfo":{"email":"janesmith@example.com","phone":"+987654321"},"addresses":[{"type":"billing","address":"789 Billing St, Metropolis, NY"},{"type":"shipping","address":"101 Shipping Blvd, Metropolis, NY"}]},"items":[{"itemId":"c0d3e4f6-b2ab-4d67-83e6-1e89c9e8a9c7","name":"Smartphone","quantity":1,"price":"899.99","attributes":[{"attributeName":"Color","attributeValue":"Gold"},{"attributeName":"Storage","attributeValue":"128GB"}],"tags":["Electronics","Mobile","Gadgets"]},{"itemId":"d1f7g8h9-2b74-4c9e-92f1-a8b9d9a8b9a2","name":"Phone Case","quantity":3,"price":"19.99","attributes":[{"attributeName":"Material","attributeValue":"Silicone"},{"attributeName":"Color","attributeValue":"Blue"}],"tags":["Accessories","Protective","Case"]}],"payment":{"method":"PayPal","status":"Pending","transactionId":"43b2e7ea-b6b4-4f7e-b1c9-8a9d8f1b7f6c"},"notes":["Awaiting payment confirmation","Customer preferred express shipping"]}],"totalOrders":2,"summary":{"totalRevenue":2145.98,"totalClients":2,"mostRecentOrderDate":"2024-09-02"}}
\ No newline at end of file
diff --git a/tests/resources/snake/data.json b/tests/resources/snake/data.json
new file mode 100644
index 0000000..64ae2b9
--- /dev/null
+++ b/tests/resources/snake/data.json
@@ -0,0 +1,1 @@
+{"date":"2024-09-03","id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","summary.most_recent_order_date":"2024-09-02","summary.total_clients":2,"summary.total_revenue":2145.98,"total_orders":2}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.client.addresses.json b/tests/resources/snake/data.orders.client.addresses.json
new file mode 100644
index 0000000..8ded2a0
--- /dev/null
+++ b/tests/resources/snake/data.orders.client.addresses.json
@@ -0,0 +1,4 @@
+{"address":"123 Billing St, Springfield, IL","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"type":"billing"}
+{"address":"456 Shipping Ave, Springfield, IL","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"type":"shipping"}
+{"address":"789 Billing St, Metropolis, NY","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"type":"billing"}
+{"address":"101 Shipping Blvd, Metropolis, NY","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"type":"shipping"}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.items.attributes.json b/tests/resources/snake/data.orders.items.attributes.json
new file mode 100644
index 0000000..af727ea
--- /dev/null
+++ b/tests/resources/snake/data.orders.items.attributes.json
@@ -0,0 +1,8 @@
+{"attribute_name":"Color","attribute_value":"Silver","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":0}
+{"attribute_name":"Warranty","attribute_value":"2 years","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":1}
+{"attribute_name":"Color","attribute_value":"Black","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":0}
+{"attribute_name":"Connectivity","attribute_value":"Bluetooth","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":1}
+{"attribute_name":"Color","attribute_value":"Gold","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":0}
+{"attribute_name":"Storage","attribute_value":"128GB","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":1}
+{"attribute_name":"Material","attribute_value":"Silicone","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":0}
+{"attribute_name":"Color","attribute_value":"Blue","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":1}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.items.json b/tests/resources/snake/data.orders.items.json
new file mode 100644
index 0000000..7206cce
--- /dev/null
+++ b/tests/resources/snake/data.orders.items.json
@@ -0,0 +1,4 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"item_id":"a7f4c3e2-df5c-4ed1-9119-9f42cba18fb9","name":"Laptop","price":"1200","quantity":1}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"item_id":"b9f8d7e9-2b60-4f1b-92d2-f8d99aefa9b3","name":"Wireless Mouse","price":"25.5","quantity":2}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"item_id":"c0d3e4f6-b2ab-4d67-83e6-1e89c9e8a9c7","name":"Smartphone","price":"899.99","quantity":1}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"item_id":"d1f7g8h9-2b74-4c9e-92f1-a8b9d9a8b9a2","name":"Phone Case","price":"19.99","quantity":3}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.items.tags.json b/tests/resources/snake/data.orders.items.tags.json
new file mode 100644
index 0000000..0b2423e
--- /dev/null
+++ b/tests/resources/snake/data.orders.items.tags.json
@@ -0,0 +1,12 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":0,"tags":"Electronics"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":1,"tags":"Portable"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":0,"index":2,"tags":"High-End"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":0,"tags":"Accessories"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":1,"tags":"Wireless"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"data.orders.items.index":1,"index":2,"tags":"Peripherals"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":0,"tags":"Electronics"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":1,"tags":"Mobile"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":0,"index":2,"tags":"Gadgets"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":0,"tags":"Accessories"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":1,"tags":"Protective"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"data.orders.items.index":1,"index":2,"tags":"Case"}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.json b/tests/resources/snake/data.orders.json
new file mode 100644
index 0000000..7f396a4
--- /dev/null
+++ b/tests/resources/snake/data.orders.json
@@ -0,0 +1,2 @@
+{"client.client_id":"5e8b25d6-7f97-4d79-a6e0-55e0e7085a62","client.contact_info.email":"johndoe@example.com","client.contact_info.phone":"+123456789","client.name":"John Doe","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-09-02","index":0,"order_id":"d6cbe4a4-b563-4c69-8473-e0f8a6636d3a","payment.card_last_four":"1234","payment.method":"Credit Card","payment.status":"Paid","payment.transaction_id":"78a0caae-b1f4-4ebf-90e4-2a1d8f9a63f1"}
+{"client.client_id":"cd8e9d76-45d2-4f32-85c9-1e68b2a8a4e7","client.contact_info.email":"janesmith@example.com","client.contact_info.phone":"+987654321","client.name":"Jane Smith","data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-08-29","index":1,"order_id":"ab5e35c1-f1a2-4bb4-badf-9f1b6f9a6b72","payment.method":"PayPal","payment.status":"Pending","payment.transaction_id":"43b2e7ea-b6b4-4f7e-b1c9-8a9d8f1b7f6c"}
\ No newline at end of file
diff --git a/tests/resources/snake/data.orders.notes.json b/tests/resources/snake/data.orders.notes.json
new file mode 100644
index 0000000..844f17c
--- /dev/null
+++ b/tests/resources/snake/data.orders.notes.json
@@ -0,0 +1,4 @@
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":0,"notes":"Order processed on priority"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":0,"index":1,"notes":"Customer requested gift wrapping"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":0,"notes":"Awaiting payment confirmation"}
+{"data.date":"2024-09-03","data.id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","data.orders.index":1,"index":1,"notes":"Customer preferred express shipping"}
\ No newline at end of file
diff --git a/tests/resources/snake/original.json b/tests/resources/snake/original.json
new file mode 100644
index 0000000..b4d8671
--- /dev/null
+++ b/tests/resources/snake/original.json
@@ -0,0 +1,1 @@
+{"id":"4f3b54b6-67ad-467c-b0ed-84a9f2c60f13","date":"2024-09-03","orders":[{"order_id":"d6cbe4a4-b563-4c69-8473-e0f8a6636d3a","date":"2024-09-02","client":{"client_id":"5e8b25d6-7f97-4d79-a6e0-55e0e7085a62","name":"John Doe","contact_info":{"email":"johndoe@example.com","phone":"+123456789"},"addresses":[{"type":"billing","address":"123 Billing St, Springfield, IL"},{"type":"shipping","address":"456 Shipping Ave, Springfield, IL"}]},"items":[{"item_id":"a7f4c3e2-df5c-4ed1-9119-9f42cba18fb9","name":"Laptop","quantity":1,"price":"1200","attributes":[{"attribute_name":"Color","attribute_value":"Silver"},{"attribute_name":"Warranty","attribute_value":"2 years"}],"tags":["Electronics","Portable","High-End"]},{"item_id":"b9f8d7e9-2b60-4f1b-92d2-f8d99aefa9b3","name":"Wireless Mouse","quantity":2,"price":"25.5","attributes":[{"attribute_name":"Color","attribute_value":"Black"},{"attribute_name":"Connectivity","attribute_value":"Bluetooth"}],"tags":["Accessories","Wireless","Peripherals"]}],"payment":{"method":"Credit Card","card_last_four":"1234","status":"Paid","transaction_id":"78a0caae-b1f4-4ebf-90e4-2a1d8f9a63f1"},"notes":["Order processed on priority","Customer requested gift wrapping"]},{"order_id":"ab5e35c1-f1a2-4bb4-badf-9f1b6f9a6b72","date":"2024-08-29","client":{"client_id":"cd8e9d76-45d2-4f32-85c9-1e68b2a8a4e7","name":"Jane Smith","contact_info":{"email":"janesmith@example.com","phone":"+987654321"},"addresses":[{"type":"billing","address":"789 Billing St, Metropolis, NY"},{"type":"shipping","address":"101 Shipping Blvd, Metropolis, NY"}]},"items":[{"item_id":"c0d3e4f6-b2ab-4d67-83e6-1e89c9e8a9c7","name":"Smartphone","quantity":1,"price":"899.99","attributes":[{"attribute_name":"Color","attribute_value":"Gold"},{"attribute_name":"Storage","attribute_value":"128GB"}],"tags":["Electronics","Mobile","Gadgets"]},{"item_id":"d1f7g8h9-2b74-4c9e-92f1-a8b9d9a8b9a2","name":"Phone Case","quantity":3,"price":"19.99","attributes":[{"attribute_name":"Material","attribute_value":"Silicone"},{"attribute_name":"Color","attribute_value":"Blue"}],"tags":["Accessories","Protective","Case"]}],"payment":{"method":"PayPal","status":"Pending","transaction_id":"43b2e7ea-b6b4-4f7e-b1c9-8a9d8f1b7f6c"},"notes":["Awaiting payment confirmation","Customer preferred express shipping"]}],"total_orders":2,"summary":{"total_revenue":2145.98,"total_clients":2,"most_recent_order_date":"2024-09-02"}}
\ No newline at end of file