Repository: aruba-uxi/rdbbeat (language: python)

Issue:
chore: prepare the code base open source
## Description

Make the library more public friendly

## Types of changes

_Put an `x` in the boxes that apply._

- [ ] Bugfix (`fix`): A non-breaking change which fixes an issue or bug
- [ ] New Feature (`feat`): A non-breaking change which adds a new feature or new functionality
- [ ] Tests (`test`): Adding missing tests or correcting existing tests
- [x] Refactoring (`refactor`): A code change that neither fixes a bug nor adds a feature, e.g Deleting unused or redundant code
- [x] Documentation (`docs`): Documentation only changes
- [ ] Other (`style`, `build`, `ci`, `performance`): Formatting, linting or styling. Changes to the CI pipeline. Performance improvements. Changes that affect the build system.

## Further Information

JIRA Ticket: <https://uxi.atlassian.net/browse/BEP-XXX>

Any relevant logs, screenshots, testing output, etc...



Discussion / hints:

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
new file mode 100644
index 0000000..3fc6df0
--- /dev/null
+++ b/.bumpversion.cfg
@@ -0,0 +1,7 @@
+[bumpversion]
+current_version = 0.1.1
+commit = True
+message = Bump version: {current_version} → {new_version}
+tag = False
+
+[bumpversion:file:setup.py]
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5bbb069
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+insert_final_newline = true
+charset = utf-8
+end_of_line = lf
+
+[Justfile]
+indent_style = tab
+
+[{*.yaml,*.yml}]
+indent_style = space
+indent_size = 2
+
+[*.tpl]
+indent_style = space
+indent_size = 2
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..c9ebadc
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,18 @@
+## Description
+
+Describe the pull request ...
+
+## Types of changes
+
+_Put an `x` in the boxes that apply._
+
+- [ ] Bugfix (`fix`): A non-breaking change which fixes an issue or bug
+- [ ] New Feature (`feat`): A non-breaking change which adds a new feature or new functionality
+- [ ] Tests (`test`): Adding missing tests or correcting existing tests
+- [ ] Refactoring (`refactor`): A code change that neither fixes a bug nor adds a feature, e.g Deleting unused or redundant code
+- [ ] Documentation (`docs`): Documentation only changes
+- [ ] Other (`style`, `build`, `ci`, `performance`): Formatting, linting or styling. Changes to the CI pipeline. Performance improvements. Changes that affect the build system.
+
+## Further Information
+
+Any relevant logs, screenshots, testing output, etc...
diff --git a/.gitignore b/.gitignore
index 9e11525..2437a35 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,13 @@ __pycache__/
 # C extensions
 *.so
 
+# JetBrains IDE's
+.idea/
+
+# VSCode IDE's
+.vscode
+.DS_Store
+
 # Distribution / packaging
 .Python
 build/
@@ -20,6 +27,8 @@ parts/
 sdist/
 var/
 wheels/
+pip-wheel-metadata/
+share/python-wheels/
 *.egg-info/
 .installed.cfg
 *.egg
@@ -34,11 +43,12 @@ MANIFEST
 # Installer logs
 pip-log.txt
 pip-delete-this-directory.txt
+.mypy_cache/
+.pytest_cache/
 
 # Unit test / coverage reports
 htmlcov/
 .tox/
-.nox/
 .coverage
 .coverage.*
 .cache
@@ -46,7 +56,6 @@ nosetests.xml
 coverage.xml
 *.cover
 .hypothesis/
-.pytest_cache/
 
 # Translations
 *.mo
@@ -55,7 +64,6 @@ coverage.xml
 # Django stuff:
 *.log
 local_settings.py
-db.sqlite3
 
 # Flask stuff:
 instance/
@@ -70,22 +78,13 @@ docs/_build/
 # PyBuilder
 target/
 
-# Jupyter Notebook
+# IPython Notebook
 .ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
+db.sqlite3
 
 # pyenv
 .python-version
 
-# celery beat schedule file
-celerybeat-schedule
-
-# SageMath parsed files
-*.sage.py
-
 # Environments
 .env
 .venv
@@ -95,26 +94,11 @@ ENV/
 env.bak/
 venv.bak/
 
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
+python-packages
 
-# mkdocs documentation
-/site
+# helm
+charts/**/*Chart.lock
+charts/**/*.tgz
 
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# my project
-.vscode
-*.db
-*.pid
-*.bak
-*.dat
-*.dir
-.pypirc
+# requirements
+dev-requirements.txt
diff --git a/.yamllint.yml b/.yamllint.yml
new file mode 100644
index 0000000..8858303
--- /dev/null
+++ b/.yamllint.yml
@@ -0,0 +1,15 @@
+extends: default
+
+rules:
+  line-length:
+    max: 100
+    ignore: |
+      /charts*/
+  document-start: disable
+  indentation:
+    # Sequences are usually not indented in the Kubernetes world
+    indent-sequences: true
+
+ignore: |
+  /.github/
+  /venv*/
diff --git a/Justfile b/Justfile
new file mode 100644
index 0000000..d7c96e5
--- /dev/null
+++ b/Justfile
@@ -0,0 +1,50 @@
+# Show this message and exit.
+help:
+	@just --list
+
+# Clean temporary files from repo folder
+clean:
+	rm -rf build dist wheels *.egg-info
+	rm -rf */build */dist
+	find . -path '*/__pycache__/*' -delete
+	find . -type d -name '__pycache__' -empty -delete
+	rm -rf '.mypy_cache'
+	rm -rf '.pytest_cache'
+	rm -rf '.coverage'
+
+# Install pip-tools
+install-pip-tools:
+	pip install --quiet pip-tools
+
+# Setup dev requirements
+setup-dev:
+	pip install -r dev-requirements.in
+	pip install -e .
+
+# Auto-format the code
+fmt:
+	isort .
+	flynt --quiet .
+	black .
+
+# Run all lints
+lint:
+	flake8 .
+	flynt --dry-run --fail-on-change --quiet .
+	isort --diff --check .
+	black --diff --check .
+	mypy .
+	yamllint .
+
+# Run tests
+test +ARGS='':
+	coverage run -m pytest {{ARGS}}
+
+# Create coverage report
+coverage:
+	coverage xml
+	coverage report
+
+# Server version bump using bumpversion (major, minor, patch)
+bumpversion +ARGS='':
+	bumpversion --config-file .bumpversion.cfg {{ARGS}}
diff --git a/LICENSE b/LICENSE
index 2a4ace5..d772eeb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,22 @@
 MIT License
 
 Copyright (c) 2018 AngelLiang
+Copyright (c) 2023 Hewlett Packard Enterprise Development LP
 
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
 
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
 
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index 3bc00da..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,19 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.org/simple"
-verify_ssl = true
-
-[dev-packages]
-autopep8 = "*"
-flake8 = "*"
-twine = "*"
-
-[packages]
-celery = ">=4.2"
-python-dotenv = "*"
-sqlalchemy = ">=1.3.0"
-mysql-connector = "*"
-celery-sqlalchemy-scheduler = {editable = true,path = "."}
-
-[requires]
-python_version = "3"
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 154dfae..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,462 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "98d02ee2e76a1ec2274050c7683c20b0633e4c5bea72b4f05943f112bd1bb0a3"
-        },
-        "pipfile-spec": 6,
-        "requires": {
-            "python_version": "3"
-        },
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "amqp": {
-            "hashes": [
-                "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
-                "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
-            ],
-            "version": "==5.0.6"
-        },
-        "billiard": {
-            "hashes": [
-                "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
-                "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
-            ],
-            "version": "==3.6.4.0"
-        },
-        "celery": {
-            "hashes": [
-                "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
-                "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"
-            ],
-            "index": "pypi",
-            "version": "==5.0.5"
-        },
-        "celery-sqlalchemy-scheduler": {
-            "editable": true,
-            "path": "."
-        },
-        "click": {
-            "hashes": [
-                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
-                "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
-            ],
-            "version": "==7.1.2"
-        },
-        "click-didyoumean": {
-            "hashes": [
-                "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
-            ],
-            "version": "==0.0.3"
-        },
-        "click-plugins": {
-            "hashes": [
-                "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b",
-                "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"
-            ],
-            "version": "==1.1.1"
-        },
-        "click-repl": {
-            "hashes": [
-                "sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5",
-                "sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"
-            ],
-            "version": "==0.1.6"
-        },
-        "greenlet": {
-            "hashes": [
-                "sha256:0a77691f0080c9da8dfc81e23f4e3cffa5accf0f5b56478951016d7cfead9196",
-                "sha256:0ddd77586553e3daf439aa88b6642c5f252f7ef79a39271c25b1d4bf1b7cbb85",
-                "sha256:111cfd92d78f2af0bc7317452bd93a477128af6327332ebf3c2be7df99566683",
-                "sha256:122c63ba795fdba4fc19c744df6277d9cfd913ed53d1a286f12189a0265316dd",
-                "sha256:181300f826625b7fd1182205b830642926f52bd8cdb08b34574c9d5b2b1813f7",
-                "sha256:1a1ada42a1fd2607d232ae11a7b3195735edaa49ea787a6d9e6a53afaf6f3476",
-                "sha256:1bb80c71de788b36cefb0c3bb6bfab306ba75073dbde2829c858dc3ad70f867c",
-                "sha256:1d1d4473ecb1c1d31ce8fd8d91e4da1b1f64d425c1dc965edc4ed2a63cfa67b2",
-                "sha256:292e801fcb3a0b3a12d8c603c7cf340659ea27fd73c98683e75800d9fd8f704c",
-                "sha256:2c65320774a8cd5fdb6e117c13afa91c4707548282464a18cf80243cf976b3e6",
-                "sha256:4365eccd68e72564c776418c53ce3c5af402bc526fe0653722bc89efd85bf12d",
-                "sha256:5352c15c1d91d22902582e891f27728d8dac3bd5e0ee565b6a9f575355e6d92f",
-                "sha256:58ca0f078d1c135ecf1879d50711f925ee238fe773dfe44e206d7d126f5bc664",
-                "sha256:5d4030b04061fdf4cbc446008e238e44936d77a04b2b32f804688ad64197953c",
-                "sha256:5d69bbd9547d3bc49f8a545db7a0bd69f407badd2ff0f6e1a163680b5841d2b0",
-                "sha256:5f297cb343114b33a13755032ecf7109b07b9a0020e841d1c3cedff6602cc139",
-                "sha256:62afad6e5fd70f34d773ffcbb7c22657e1d46d7fd7c95a43361de979f0a45aef",
-                "sha256:647ba1df86d025f5a34043451d7c4a9f05f240bee06277a524daad11f997d1e7",
-                "sha256:719e169c79255816cdcf6dccd9ed2d089a72a9f6c42273aae12d55e8d35bdcf8",
-                "sha256:7cd5a237f241f2764324396e06298b5dee0df580cf06ef4ada0ff9bff851286c",
-                "sha256:875d4c60a6299f55df1c3bb870ebe6dcb7db28c165ab9ea6cdc5d5af36bb33ce",
-                "sha256:90b6a25841488cf2cb1c8623a53e6879573010a669455046df5f029d93db51b7",
-                "sha256:94620ed996a7632723a424bccb84b07e7b861ab7bb06a5aeb041c111dd723d36",
-                "sha256:b5f1b333015d53d4b381745f5de842f19fe59728b65f0fbb662dafbe2018c3a5",
-                "sha256:c5b22b31c947ad8b6964d4ed66776bcae986f73669ba50620162ba7c832a6b6a",
-                "sha256:c93d1a71c3fe222308939b2e516c07f35a849c5047f0197442a4d6fbcb4128ee",
-                "sha256:cdb90267650c1edb54459cdb51dab865f6c6594c3a47ebd441bc493360c7af70",
-                "sha256:cfd06e0f0cc8db2a854137bd79154b61ecd940dce96fad0cba23fe31de0b793c",
-                "sha256:d3789c1c394944084b5e57c192889985a9f23bd985f6d15728c745d380318128",
-                "sha256:da7d09ad0f24270b20f77d56934e196e982af0d0a2446120cb772be4e060e1a2",
-                "sha256:df3e83323268594fa9755480a442cabfe8d82b21aba815a71acf1bb6c1776218",
-                "sha256:df8053867c831b2643b2c489fe1d62049a98566b1646b194cc815f13e27b90df",
-                "sha256:e1128e022d8dce375362e063754e129750323b67454cac5600008aad9f54139e",
-                "sha256:e6e9fdaf6c90d02b95e6b0709aeb1aba5affbbb9ccaea5502f8638e4323206be",
-                "sha256:eac8803c9ad1817ce3d8d15d1bb82c2da3feda6bee1153eec5c58fa6e5d3f770",
-                "sha256:eb333b90036358a0e2c57373f72e7648d7207b76ef0bd00a4f7daad1f79f5203",
-                "sha256:ed1d1351f05e795a527abc04a0d82e9aecd3bdf9f46662c36ff47b0b00ecaf06",
-                "sha256:f3dc68272990849132d6698f7dc6df2ab62a88b0d36e54702a8fd16c0490e44f",
-                "sha256:f59eded163d9752fd49978e0bab7a1ff21b1b8d25c05f0995d140cc08ac83379",
-                "sha256:f5e2d36c86c7b03c94b8459c3bd2c9fe2c7dab4b258b8885617d44a22e453fb7",
-                "sha256:f6f65bf54215e4ebf6b01e4bb94c49180a589573df643735107056f7a910275b",
-                "sha256:f8450d5ef759dbe59f84f2c9f77491bb3d3c44bc1a573746daf086e70b14c243",
-                "sha256:f97d83049715fd9dec7911860ecf0e17b48d8725de01e45de07d8ac0bd5bc378"
-            ],
-            "markers": "python_version >= '3'",
-            "version": "==1.0.0"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef",
-                "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==4.0.0"
-        },
-        "kombu": {
-            "hashes": [
-                "sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
-                "sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
-            ],
-            "version": "==5.0.2"
-        },
-        "mysql-connector": {
-            "hashes": [
-                "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"
-            ],
-            "index": "pypi",
-            "version": "==2.2.9"
-        },
-        "prompt-toolkit": {
-            "hashes": [
-                "sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
-                "sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
-            ],
-            "version": "==3.0.18"
-        },
-        "python-dotenv": {
-            "hashes": [
-                "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a",
-                "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"
-            ],
-            "index": "pypi",
-            "version": "==0.17.0"
-        },
-        "pytz": {
-            "hashes": [
-                "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
-                "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
-            ],
-            "version": "==2021.1"
-        },
-        "six": {
-            "hashes": [
-                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
-                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
-            ],
-            "version": "==1.15.0"
-        },
-        "sqlalchemy": {
-            "hashes": [
-                "sha256:065ac7331b87494a86bf3dc4430c1ee7779d6dc532213c528394ddd00804e518",
-                "sha256:099e63ffad329989080c533896267c40f9cb38ed5704168f7dae3afdda121e10",
-                "sha256:0d8aab144cf8d31c1ac834802c7df4430248f74bd8b3ed3149f9c9eec0eafe50",
-                "sha256:230b210fc6d1af5d555d1d04ff9bd4259d6ab82b020369724ab4a1c805a32dd3",
-                "sha256:25aaf0bec9eadde9789e3c0178c718ae6923b57485fdeae85999bc3089d9b871",
-                "sha256:29816a338982c30dd7ee76c4e79f17d5991abb1b6561e9f1d72703d030a79c86",
-                "sha256:2e1b8d31c97a2b91aea8ed8299ad360a32d60728a89f2aac9c98eef07a633a0e",
-                "sha256:343c679899afdc4952ac659dc46f2075a2bd4fba87ca0df264be838eecd02096",
-                "sha256:386f215248c3fb2fab9bb77f631bc3c6cd38354ca2363d241784f8297d16b80a",
-                "sha256:457a1652bc1c5f832165ff341380b3742bfb98b9ceca24576350992713ad700f",
-                "sha256:4e554872766d2783abf0a11704536596e8794229fb0fa63d311a74caae58c6c5",
-                "sha256:4edff2b4101a1c442fb1b17d594a5fdf99145f27c5eaffae12c26aef2bb2bf65",
-                "sha256:690fbca2a208314504a2ab46d3e7dae320247fcb1967863b9782a70bf49fc600",
-                "sha256:6c6090d73820dcf04549f0b6e80f67b46c8191f0e40bf09c6d6f8ece2464e8b6",
-                "sha256:7bdb0f972bc35054c05088e91cec8fa810c3aa565b690bae75c005ee430e12e8",
-                "sha256:815a8cdf9c0fa504d0bfbe83fb3e596b7663fc828b73259a20299c01330467aa",
-                "sha256:a28c7b96bc5beef585172ca9d79068ae7fa2527feaa26bd63371851d7894c66f",
-                "sha256:a8763fe4de02f746666161b130cc3e5d1494a6f5475f5622f05251739fc22e55",
-                "sha256:b0266e133d819d33b555798822606e876187a96798e2d8c9b7f85e419d73ef94",
-                "sha256:bb97aeaa699c43da62e35856ab56e5154d062c09a3593a2c12c67d6a21059920",
-                "sha256:bce6eaf7b9a3a445911e225570b8fd26b7e98654ac9f308a8a52addb64a2a488",
-                "sha256:c4485040d86d4b3d9aa509fd3c492de3687d9bf52fb85d66b33912ad068a088c",
-                "sha256:c6f228b79fd757d9ca539c9958190b3a44308f743dc7d83575aa0891033f6c86",
-                "sha256:cde2cf3ee76e8c538f2f43f5cf9252ad53404fc350801191128bab68f335a8b2",
-                "sha256:cfa4a336de7d32ae30b54f7b8ec888fb5c6313a1b7419a9d7b3f49cdd83012a3",
-                "sha256:cfbf2cf8e8ef0a1d23bfd0fa387057e6e522d55e43821f1d115941d913ee7762",
-                "sha256:e26791ac43806dec1f18d328596db87f1b37f9d8271997dd1233054b4c377f51",
-                "sha256:e7d262415e4adf148441bd9f10ae4e5498d6649962fabc62a64ec7b4891d56c5",
-                "sha256:e9e95568eafae18ac40d00694b82dc3febe653f81eee83204ef248563f39696d",
-                "sha256:ec7c33e22beac16b4c5348c41cd94cfee056152e55a0efc62843deebfc53fcb4",
-                "sha256:f239778cf03cd46da4962636501f6dea55af9b4684cd7ceee104ad4f0290e878",
-                "sha256:f31757972677fbe9132932a69a4f23db59187a072cc26427f56a3082b46b6dac",
-                "sha256:fbdcf9019e92253fc6aa0bcd5937302664c3a4d53884c425c0caa994e56c4421",
-                "sha256:fc82688695eacf77befc3d839df2bc7ff314cd1d547f120835acdcbac1a480b8"
-            ],
-            "index": "pypi",
-            "version": "==1.4.9"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
-                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
-                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==3.7.4.3"
-        },
-        "vine": {
-            "hashes": [
-                "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
-                "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
-            ],
-            "version": "==5.0.0"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
-                "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
-            ],
-            "version": "==0.2.5"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
-                "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
-            ],
-            "version": "==3.4.1"
-        }
-    },
-    "develop": {
-        "autopep8": {
-            "hashes": [
-                "sha256:5454e6e9a3d02aae38f866eec0d9a7de4ab9f93c10a273fb0340f3d6d09f7514",
-                "sha256:f01b06a6808bc31698db907761e5890eb2295e287af53f6693b39ce55454034a"
-            ],
-            "index": "pypi",
-            "version": "==1.5.6"
-        },
-        "bleach": {
-            "hashes": [
-                "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125",
-                "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"
-            ],
-            "version": "==3.3.0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
-                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
-            ],
-            "version": "==2020.12.5"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
-            ],
-            "version": "==4.0.0"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
-                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
-            ],
-            "version": "==0.4.4"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
-                "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
-            ],
-            "version": "==0.17.1"
-        },
-        "flake8": {
-            "hashes": [
-                "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378",
-                "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"
-            ],
-            "index": "pypi",
-            "version": "==3.9.1"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
-                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
-            ],
-            "version": "==2.10"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:19192b88d959336bfa6bdaaaef99aeafec179eca19c47c804e555703ee5f07ef",
-                "sha256:2e881981c9748d7282b374b68e759c87745c25427b67ecf0cc67fb6637a1bff9"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==4.0.0"
-        },
-        "keyring": {
-            "hashes": [
-                "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8",
-                "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"
-            ],
-            "version": "==23.0.1"
-        },
-        "mccabe": {
-            "hashes": [
-                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
-                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
-            ],
-            "version": "==0.6.1"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
-                "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
-            ],
-            "version": "==20.9"
-        },
-        "pkginfo": {
-            "hashes": [
-                "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4",
-                "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75"
-            ],
-            "version": "==1.7.0"
-        },
-        "pycodestyle": {
-            "hashes": [
-                "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
-                "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
-            ],
-            "version": "==2.7.0"
-        },
-        "pyflakes": {
-            "hashes": [
-                "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
-                "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
-            ],
-            "version": "==2.3.1"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:2656e1a6edcdabf4275f9a3640db59fd5de107d88e8663c5d4e9a0fa62f77f94",
-                "sha256:534ef71d539ae97d4c3a4cf7d6f110f214b0e687e92f9cb9d2a3b0d3101289c8"
-            ],
-            "version": "==2.8.1"
-        },
-        "pyparsing": {
-            "hashes": [
-                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
-                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
-            ],
-            "version": "==2.4.7"
-        },
-        "pywin32-ctypes": {
-            "hashes": [
-                "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
-                "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
-            ],
-            "markers": "sys_platform == 'win32'",
-            "version": "==0.2.0"
-        },
-        "readme-renderer": {
-            "hashes": [
-                "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
-                "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
-            ],
-            "version": "==29.0"
-        },
-        "requests": {
-            "hashes": [
-                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
-                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
-            ],
-            "version": "==2.25.1"
-        },
-        "requests-toolbelt": {
-            "hashes": [
-                "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
-                "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
-            ],
-            "version": "==0.9.1"
-        },
-        "rfc3986": {
-            "hashes": [
-                "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
-                "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
-            ],
-            "version": "==1.4.0"
-        },
-        "six": {
-            "hashes": [
-                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
-                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
-            ],
-            "version": "==1.15.0"
-        },
-        "toml": {
-            "hashes": [
-                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
-                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
-            ],
-            "version": "==0.10.2"
-        },
-        "tqdm": {
-            "hashes": [
-                "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3",
-                "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae"
-            ],
-            "version": "==4.60.0"
-        },
-        "twine": {
-            "hashes": [
-                "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83",
-                "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b"
-            ],
-            "index": "pypi",
-            "version": "==3.4.1"
-        },
-        "typing-extensions": {
-            "hashes": [
-                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
-                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
-                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
-            ],
-            "markers": "python_version < '3.8'",
-            "version": "==3.7.4.3"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
-                "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
-            ],
-            "version": "==1.26.4"
-        },
-        "webencodings": {
-            "hashes": [
-                "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
-                "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
-            ],
-            "version": "==0.5.1"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
-                "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
-            ],
-            "version": "==3.4.1"
-        }
-    }
-}
diff --git a/README-zh.md b/README-zh.md
deleted file mode 100644
index e055f57..0000000
--- a/README-zh.md
+++ /dev/null
@@ -1,228 +0,0 @@
-# celery-sqlalchemy-scheduler
-
-一个基于 sqlalchemy 的 scheduler，作为 celery 定时任务的辅助工具。
-
-## 快速开始
-
-[中文文档](/README-zh.md) [English](/README.md)
-
-### 依赖
-
-- Python 3
-- celery >= 4.2
-- sqlalchemy
-
-首先必须安装 `celery` 和 `sqlalchemy`, 并且`celery`应该大于等于 4.2 版本。
-
-```
-$ pip install sqlalchemy celery
-```
-
-### 安装
-
-通过 PyPi 安装：
-
-```
-$ pip install celery-sqlalchemy-scheduler
-```
-
-通过 github 仓库进行安装：
-
-```
-$ git clone git@github.com:AngelLiang/celery-sqlalchemy-scheduler.git
-$ cd celery-sqlalchemy-scheduler
-$ python setup.py install
-```
-
-## 使用示例
-
-安装`celery_sqlalchemy_scheduler`之后，你可以查看`examples`目录下的代码：
-
-1. 启动 celery worker：
-
-   ```
-   $ celery worker -A tasks -l info
-   ```
-
-2. 使用`DatabaseScheduler`作为 scheduler 启动 celery beat：
-
-   ```
-   $ celery beat -A tasks -S celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler -l info
-   ```
-
-## 使用说明
-
-beat 启动之后，默认会在当前目录下生成名称为`schedule.db`的 sqlite 数据库。Windows 下可以使用 SQLiteStudio.exe 工具打开查看里面的数据。
-
-![sqlite](screenshot/sqlite.png)
-
-### 数据库同步 scheduler 到 beat
-
-当需要更新 scheduler，只需要修改`schedule.db`相关数据即可。修改好数据库的 scheduler 后，`celery_sqlalchemy_scheduler`并不会马上同步数据库的数据到 beat，我们最后还需要修改`celery_periodic_task_changed`表的第一条数据，只需要把`last_update`字段更新到最新的时间即可。当 beat 在下一个“心跳”之后，就会同步数据库的数据到 beat。
-
-## 配置数据库
-
-在配置 Celery 的时候，可以设置 sqlalchemy 数据库的路径，示例如下：
-
-```Python
-from celery import Celery
-
-celery = Celery('tasks')
-
-beat_dburi = 'sqlite:///schedule.db'
-
-celery.conf.update(
-    {'beat_dburi': beat_dburi}
-)
-```
-
-当然，你可以改为使用 MySQL 或 PostgreSQL：
-
-```Python
-# MySQL: `pip install mysql-connector`
-beat_dburi = 'mysql+mysqlconnector://root:root@127.0.0.1:3306/celery-schedule'
-
-# PostgreSQL: `pip install psycopg2`
-beat_dburi = 'postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/celery-schedule'
-```
-
-## 示例代码一
-
-可以查看 `examples/base/tasks.py` 代码获取相关细节说明
-
-相关讨论：https://github.com/AngelLiang/celery-sqlalchemy-scheduler/issues/15#issuecomment-625624088
-
-开一个console，启动 Celery Worker
-
-    $ pipenv shell
-    $ cd examples/base
-
-    # Celery < 5.0
-    $ celery worker -A tasks:celery -l info
-
-    # Celery >= 5.0
-    $ celery -A tasks:celery worker -l info
-
-开第二个console，启动 Celery Beat
-
-    $ pipenv shell
-    $ cd examples/base
-
-    # Celery < 5.0
-    $ celery beat -A tasks:celery -S tasks:DatabaseScheduler -l info
-
-    # Celery >= 5.0
-    $ celery -A tasks:celery beat -S tasks:DatabaseScheduler -l info
-
-## 示例代码二
-
-### `examples/base/tasks.py`
-
-```python
-# coding=utf-8
-"""
-Ready::
-
-    $ pipenv install
-    $ pipenv shell
-    $ python setup.py install
-
-Run Worker::
-
-    $ cd examples/base
-    $ celery worker -A tasks:celery -l info
-
-Run Beat::
-
-    $ cd examples/base
-    $ celery beat -A tasks:celery -S tasks:DatabaseScheduler -l info
-
-"""
-import os
-import platform
-from datetime import timedelta
-from celery import Celery
-from celery import schedules
-
-from celery_sqlalchemy_scheduler.schedulers import DatabaseScheduler  # noqa
-
-if platform.system() == 'Windows':
-    # Celery在Windows环境下运行需要设置这个变量，否则调用任务会报错
-    os.environ['FORKED_BY_MULTIPROCESSING'] = '1'
-
-backend = 'rpc://'
-broker_url = 'amqp://guest:guest@localhost:5672//'
-
-# 如果数据库修改了下面的schedule，beat重启后数据库会被下面的配置覆盖
-beat_schedule = {
-    'echo-every-3-seconds': {
-        'task': 'tasks.echo',
-        'schedule': timedelta(seconds=3),
-        'args': ('hello', )
-    },
-    'add-every-minutes': {
-        'task': 'tasks.add',
-        'schedule': schedules.crontab('*', '*', '*'),
-        'args': (1, 2)
-    },
-}
-
-beat_scheduler = 'celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler'
-
-beat_sync_every = 0
-
-# The maximum number of seconds beat can sleep between checking the schedule.
-# default: 0
-beat_max_loop_interval = 10
-
-# 非celery和beat的配置，配置beat_dburi数据库路径
-beat_dburi = 'sqlite:///schedule.db'
-# OR
-# beat_dburi = 'mysql+mysqlconnector://root:root@127.0.0.1/celery-schedule'
-
-# 配置时区
-timezone = 'Asia/Shanghai'
-
-# 默认每个worker跑完10个任务后，自我销毁程序重建来释放内存
-# 防止内存泄漏
-worker_max_tasks_per_child = 10
-
-celery = Celery('tasks',
-                backend=backend,
-                broker=broker_url)
-
-config = dict(
-    beat_schedule=beat_schedule,
-    beat_scheduler=beat_scheduler,
-    beat_max_loop_interval=beat_max_loop_interval,
-    beat_dburi=beat_dburi,
-
-    timezone=timezone,
-    worker_max_tasks_per_child=worker_max_tasks_per_child
-)
-
-celery.conf.update(config)
-
-
-@celery.task
-def add(x, y):
-    return x + y
-
-
-@celery.task
-def echo(data):
-    print(data)
-
-
-if __name__ == "__main__":
-    celery.start()
-
-```
-
-## 参考
-
-本工具主要参考了以下资料和源码：
-
-- [django-celery-beat](https://github.com/celery/django-celery-beat)
-- [celerybeatredis](https://github.com/liuliqiang/celerybeatredis)
-- [celery](https://github.com/celery/celery)
diff --git a/README.md b/README.md
index c1249df..891e184 100644
--- a/README.md
+++ b/README.md
@@ -1,262 +1,166 @@
-# celery-sqlalchemy-scheduler
+[![Python Version](https://img.shields.io/badge/python-3.8-blue?logo=Python&logoColor=yellow)](https://docs.python.org/3.8/)
+[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+[![Build: just](https://img.shields.io/badge/%F0%9F%A4%96%20build-just-black?labelColor=white)](https://just.systems/)
+[![code](https://github.com/aruba-uxi/celery-sqlalchemy-scheduler/actions/workflows/lint-test-code.yaml/badge.svg)](https://github.com/aruba-uxi/celery-sqlalchemy-scheduler/actions/workflows/lint-test-code.yaml)
 
-A Scheduler Based Sqlalchemy for Celery.
+# RDBBeat
 
-> NOTE: At first I developed this project for flask with celery to change scheduler from database, like [django-celery-beat](https://github.com/celery/django-celery-beat) for django. And now I maybe haven't time to develop for new feature. No new feature develop plan for it. Just fix bugs. If someone found some bugs, welcome to issue or PR. Thank you for your attention.
+`rdbbeat` is a SQLAlchemy-based scheduler for celery-beat that persists periodic tasks in a relational database.
 
-## Getting Started
+## Table Of Contents
 
-[English](/README.md) [中文文档](/README-zh.md)
+- [Installation](#installation)
+- [Celery Configuration](#celery-configuration)
+- [Usage](#usage)
+- [Deployment](#deployment)
+- [Flask Service Example](#flask-service-example)
+- [Contribution](#contribution)
+    - [Setup](#setup)
+    - [Version Control](#version-control)
+    - [Workflows](#workflows)
+- [License](#license)
+- [Acknowledgements](#acknowledgements)
 
-### Prerequisites
+> **__NOTE__:** We assume prior knowledge of session management in SQLAlchemy and migrations in Alembic. If you are not familiar with these concepts, please read the documentation for [SQLAlchemy](https://docs.sqlalchemy.org/en/14/orm/session_basics.html) and [Alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html).
 
-- Python 3
-- celery >= 4.2
-- sqlalchemy
+## Installation
 
-First you must install `celery` and `sqlalchemy`, and `celery` should be >=4.2.0.
+The library is available for download from the official pypi website:
 
-```
-$ pip install sqlalchemy celery
-```
-
-### Installing
-
-Install from PyPi:
-
-```
-$ pip install celery-sqlalchemy-scheduler
-```
-
-Install from source by cloning this repository:
-
-```
-$ git clone git@github.com:AngelLiang/celery-sqlalchemy-scheduler.git
-$ cd celery-sqlalchemy-scheduler
-$ python setup.py install
-```
-
-## Usage
-
-After you have installed `celery_sqlalchemy_scheduler`, you can easily start with following steps:
-
-This is a demo for exmaple, you can check the code in `examples` directory
-
-1. start celery worker
+`pip install rdbbeat`
 
-   ```
-   $ celery worker -A tasks -l info
-   ```
+Or you can build the library for the source code hosted publicly on GitHub.
 
-2. start the celery beat with `DatabaseScheduler` as scheduler:
 
-   ```
-   $ celery beat -A tasks -S celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler -l info
-   ```
+## Celery Configuration
 
-## Description
 
-After the celery beat is started, by default it create a sqlite database(`schedule.db`) in current folder. You can use `SQLiteStudio.exe` to inspect it.
-
-![sqlite](screenshot/sqlite.png)
-
-When you want to update scheduler, you can update the data in `schedule.db`. But `celery_sqlalchemy_scheduler` don't update the scheduler immediately. Then you shoule be change the first column's `last_update` field in the `celery_periodic_task_changed` to now datetime. Finally the celery beat will update scheduler at next wake-up time.
-
-### Database Configuration
-
-You can configure sqlalchemy db uri when you configure the celery, example as:
+The library makes use of the parent service's database and scope management mechanism.
+You can configure SQLAlchemy `session_scope` when you configure celery, for example as:
 
 ```Python
 from celery import Celery
 
-celery = Celery('tasks')
+# this comes from parent service
+from server.db import session_scope
 
-beat_dburi = 'sqlite:///schedule.db'
+celery = Celery('tasks')
 
 celery.conf.update(
-    {'beat_dburi': beat_dburi}
+    {'session_scope': session_scope}
 )
 ```
 
-Also, you can use MySQL or PostgreSQL.
+## Usage
+### Creating crontab-based periodic task
 
-```Python
-# MySQL: `pip install mysql-connector`
-beat_dburi = 'mysql+mysqlconnector://root:root@127.0.0.1:3306/celery-schedule'
+A crontab schedule has the fields: `minute`, `hour`, `day_of_week`,
+`day_of_month` and `month_of_year`, so if you want the equivalent of a
+`30 * * * *` (execute every 30 minutes) crontab entry, you specify:
 
-# PostgreSQL: `pip install psycopg2`
-beat_dburi = 'postgresql+psycopg2://postgres:postgres@127.0.0.1:5432/celery-schedule'
+```Python
+from rdbbeat.controller import schedule_task
+from rdbbeat.data_models import ScheduledTask
+
+scheduled_task = {
+    "name": "task_1",
+    "task": "echo",
+    "schedule": {
+        "minute": "30",
+        "hour": "*",
+        "day_of_week": "*",
+        "day_of_month": "*",
+        "month_of_year": "*",
+        "timezone": "UTC",
+    },
+}
+
+with session_scope() as session:
+    task = schedule_task(session, ScheduledTask.parse_ob(scheduled_task))
 ```
 
-## Example Code 1
-
-View `examples/base/tasks.py` for details.
-
-How to quickstart: https://github.com/AngelLiang/celery-sqlalchemy-scheduler/issues/15#issuecomment-625624088
-
-Run Worker in console 1
+The crontab schedule is linked to a specific timezone using the
+`timezone` input parameter.
 
-    $ pipenv shell
-    $ cd examples/base
+## Run the migrations
 
-    # Celery < 5.0
-    $ celery worker -A tasks:celery -l info
+`rdbbeat` includes a migration script that is required to create the database tables in the `scheduler` schema. Run the following command to run the migrations:
 
-    # Celery >= 5.0
-    $ celery -A tasks:celery worker -l info
+```sh
+python -m alembic -n scheduler upgrade head
+```
+## Deployment
 
-Run Beat in console 2
+The periodic tasks still need 'workers' to execute them. So make sure
+the default **Celery** package is installed. (If not installed, please
+follow the installation instructions here:
+<https://github.com/celery/celery>)
 
-    $ pipenv shell
-    $ cd examples/base
+Both the worker and beat services need to be running at the same time.
 
-    # Celery < 5.0
-    $ celery beat -A tasks:celery -S tasks:DatabaseScheduler -l info
+1.  Start a Celery worker service (specify your project name):
 
-    # Celery >= 5.0
-    $ celery -A tasks:celery beat -S tasks:DatabaseScheduler -l info
+        $ celery -A [project-name] worker --loglevel=info
 
-## Example Code 2
+2.  As a separate process, start the beat service (specify the
+    scheduler):
 
-### Example creating interval-based periodic task
+        $ celery -A [project-name] beat -l info --scheduler rdbbeat.schedulers:DatabaseScheduler
 
-To create a periodic task executing at an interval you must first
-create the interval object:
+## Flask Service Example
 
-```python
->>> from celery_sqlalchemy_scheduler.models import PeriodicTask, IntervalSchedule
->>> from celery_sqlalchemy_scheduler.session import SessionManager
->>> from celeryconfig import beat_dburi
->>> session_manager = SessionManager()
->>> engine, Session = session_manager.create_session(beat_dburi)
->>> session = Session()
 
-# executes every 10 seconds.
->>> schedule = session.query(IntervalSchedule).filter_by(every=10, period=IntervalSchedule.SECONDS).first()
->>> if not schedule:
-...     schedule = IntervalSchedule(every=10, period=IntervalSchedule.SECONDS)
-...     session.add(schedule)
-...     session.commit()
-```
+Check out the [rdbbeat-flask-example](https://github.com/evanstjabadi/rdbbeat-flask-example) repo for a simple Flask service that uses `rdbbeat` to schedule periodic tasks.
 
-That's all the fields you need: a period type and the frequency.
+# Contribution
+## Setup
 
-You can choose between a specific set of periods:
 
-- `IntervalSchedule.DAYS`
-- `IntervalSchedule.HOURS`
-- `IntervalSchedule.MINUTES`
-- `IntervalSchedule.SECONDS`
-- `IntervalSchedule.MICROSECONDS`
 
-_note_:
+This project is setup to use [editorconfig](https://editorconfig.org/). Most editors work but some require a plugin like [VSCode](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)
 
-    If you have multiple periodic tasks executing every 10 seconds,
-    then they should all point to the same schedule object.
+It's advisable to create a virtual environment for this project to keep packages separate.
+> **__NOTE__:** Using pyenv, you can run `pyenv virtualenv 3.10.<latest> celery-sqlalchemy-scheduler`
 
-Now that we have defined the schedule object, we can create the periodic task
-entry:
+After creating a virtual environment, install the required dependencies.
 
-```python
-    >>> task = PeriodicTask(
-    ...     interval=schedule,                  # we created this above.
-    ...     name='Importing contacts',          # simply describes this periodic task.
-    ...     task='proj.tasks.import_contacts',  # name of task.
-    ... )
-    >>> session.add(task)
-    >>> session.commit()
+```sh
+just setup-dev
 ```
 
-Note that this is a very basic example, you can also specify the
-arguments and keyword arguments used to execute the task, the `queue` to
-send it to[\*], and set an expiry time.
-
-Here\'s an example specifying the arguments, note how JSON serialization
-is required:
-
-    >>> import json
-    >>> from datetime import datetime, timedelta
-
-    >>> periodic_task = PeriodicTask(
-    ...     interval=schedule,                  # we created this above.
-    ...     name='Importing contacts',          # simply describes this periodic task.
-    ...     task='proj.tasks.import_contacts',  # name of task.
-    ...     args=json.dumps(['arg1', 'arg2']),
-    ...     kwargs=json.dumps({
-    ...        'be_careful': True,
-    ...     }),
-    ...     expires=datetime.utcnow() + timedelta(seconds=30)
-    ... )
-    ... session.add(periodic_task)
-    ... session.commit()
-
-### Example creating crontab-based periodic task
+## Version Control
 
-A crontab schedule has the fields: `minute`, `hour`, `day_of_week`,
-`day_of_month` and `month_of_year`, so if you want the equivalent of a
-`30 * * * *` (execute every 30 minutes) crontab entry you specify:
-
-    >>> from celery_sqlalchemy_scheduler.models import PeriodicTask, CrontabSchedule
-    >>> schedule = CrontabSchedule(
-    ...     minute='30',
-    ...     hour='*',
-    ...     day_of_week='*',
-    ...     day_of_month='*',
-    ...     month_of_year='*',
-    ...     timezone='UTC',
-    ... )
+This repo follows the [SemVer 2](https://semver.org/) version format.
 
-The crontab schedule is linked to a specific timezone using the
-'timezone' input parameter.
+Given a version number `MAJOR.MINOR.PATCH`, increment the:
 
-Then to create a periodic task using this schedule, use the same
-approach as the interval-based periodic task earlier in this document,
-but instead of `interval=schedule`, specify `crontab=schedule`:
+- `MAJOR` version when you make incompatible API changes,
+- `MINOR` version when you add functionality in a backwards compatible manner, and
+- `PATCH` version when you make backwards compatible bug fixes.
 
-    >>> periodic_task = PeriodicTask(
-    ...     crontab=schedule,
-    ...     name='Importing contacts',
-    ...     task='proj.tasks.import_contacts',
-    ... )
-    ... session.add(periodic_task)
-    ... session.commit()
+## Workflows
 
-### Temporarily disable a periodic task
+The repository has a number of github workflows defined in the the `.github/workflows` folder.
 
-You can use the `enabled` flag to temporarily disable a periodic task:
 
-    >>> periodic_task.enabled = False
-    >>> session.add(periodic_task)
-    >>> session.commit()
 
-> Note: If you want to delete `PeriodicTask`, don't use `.delete()` method on a query
-> such as `db.session.query(PeriodicTask).filter(PeriodicTask.id == task_id).delete()`.
-> Because it doesn't trigger the `after_delete` event listener and result in Error.
-> The correct deletion method is using session to delete `PeriodicTask` object.
+### Lint & Test Code
 
-    >>> db.session.delete(db.session.query(PeriodicTask).get(task_id))
-    >>> db.session.commit()
+- Tests the code for linting issues
+- Tests the requirements file for any changes
+- Runs the provided unit tests to ensure code quality
 
-### Example running periodic tasks
+### Release
 
-The periodic tasks still need 'workers' to execute them. So make sure
-the default **Celery** package is installed. (If not installed, please
-follow the installation instructions here:
-<https://github.com/celery/celery>)
+- Pushes the library to Pypi
 
-Both the worker and beat services need to be running at the same time.
-
-1.  Start a Celery worker service (specify your project name):
-
-        $ celery -A [project-name] worker --loglevel=info
-
-2.  As a separate process, start the beat service (specify the
-    scheduler):
 
-        $ celery -A [project-name] beat -l info --scheduler celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler
+## License
+MIT licensed. See the bundled LICENSE file for more details.
 
-## Acknowledgments
+## Acknowledgements
 
 - [django-celery-beat](https://github.com/celery/django-celery-beat)
 - [celerybeatredis](https://github.com/liuliqiang/celerybeatredis)
 - [celery](https://github.com/celery/celery)
+- [celery-sqlalchemy-scheduler](https://github.com/AngelLiang/celery-sqlalchemy-scheduler)
diff --git a/alembic.ini b/alembic.ini
new file mode 100644
index 0000000..f56aef9
--- /dev/null
+++ b/alembic.ini
@@ -0,0 +1,103 @@
+[alembic]
+# path to migration scripts
+script_location = rdbbeat/db/migrations
+
+# template used to generate migration files
+file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(slug)s
+
+# sys.path path, will be prepended to sys.path if present.
+# defaults to the current working directory.
+prepend_sys_path = .
+
+# timezone to use when rendering the date within the migration file
+# as well as the filename.
+# If specified, requires the python-dateutil library that can be
+# installed by adding `alembic[tz]` to the pip requirements
+# string value is passed to dateutil.tz.gettz()
+# leave blank for localtime
+# timezone =
+
+# max length of characters to apply to the
+# "slug" field
+# truncate_slug_length = 40
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+# set to 'true' to allow .pyc and .pyo files without
+# a source .py file to be detected as revisions in the
+# versions/ directory
+# sourceless = false
+
+# version location specification; This defaults
+# to migrations/versions.  When using multiple version
+# directories, initial revisions must be specified with --version-path.
+# The path separator used here should be the separator specified by "version_path_separator" below.
+# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
+
+# version path separator; As mentioned above, this is the character used to split
+# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
+# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
+# Valid values for version_path_separator are:
+#
+# version_path_separator = :
+# version_path_separator = ;
+# version_path_separator = space
+version_path_separator = os  # Use os.pathsep. Default configuration used for new projects.
+
+# the output encoding used when revision files
+# are written from script.py.mako
+# output_encoding = utf-8
+
+# Note: set up some empty database before doing migrations and provide the details here
+sqlalchemy.url = postgresql://tabloid_service:dev@localhost:5026/tabloid_service
+
+version_table = alembic_revision
+
+
+[post_write_hooks]
+# post_write_hooks defines scripts or Python functions that are run
+# on newly generated revision scripts.  See the documentation for further
+# detail and examples
+
+# format using "black" - use the console_scripts runner, against the "black" entrypoint
+# hooks = black
+# black.type = console_scripts
+# black.entrypoint = black
+# black.options = -l 79 REVISION_SCRIPT_FILENAME
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/celery_sqlalchemy_scheduler/__init__.py b/celery_sqlalchemy_scheduler/__init__.py
deleted file mode 100644
index dc35ef5..0000000
--- a/celery_sqlalchemy_scheduler/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# coding=utf-8
-# flake8:noqa
-
-from .session import SessionManager
-from .models import (
-    PeriodicTask, PeriodicTaskChanged,
-    CrontabSchedule, IntervalSchedule,
-    SolarSchedule,
-)
-from .schedulers import DatabaseScheduler
diff --git a/celery_sqlalchemy_scheduler/models.py b/celery_sqlalchemy_scheduler/models.py
deleted file mode 100644
index 530a32b..0000000
--- a/celery_sqlalchemy_scheduler/models.py
+++ /dev/null
@@ -1,314 +0,0 @@
-# coding=utf-8
-
-import datetime as dt
-import pytz
-
-import sqlalchemy as sa
-from sqlalchemy import func
-from sqlalchemy.event import listen
-from sqlalchemy.orm import relationship, foreign, remote
-from sqlalchemy.sql import select, insert, update
-
-from celery import schedules
-from celery.utils.log import get_logger
-
-from .tzcrontab import TzAwareCrontab
-from .session import ModelBase
-
-
-logger = get_logger('celery_sqlalchemy_scheduler.models')
-
-
-def cronexp(field):
-    """Representation of cron expression."""
-    return field and str(field).replace(' ', '') or '*'
-
-
-class ModelMixin(object):
-
-    @classmethod
-    def create(cls, **kw):
-        return cls(**kw)
-
-    def update(self, **kw):
-        for attr, value in kw.items():
-            setattr(self, attr, value)
-        return self
-
-
-class IntervalSchedule(ModelBase, ModelMixin):
-    __tablename__ = 'celery_interval_schedule'
-    __table_args__ = {'sqlite_autoincrement': True}
-
-    DAYS = 'days'
-    HOURS = 'hours'
-    MINUTES = 'minutes'
-    SECONDS = 'seconds'
-    MICROSECONDS = 'microseconds'
-
-    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
-
-    every = sa.Column(sa.Integer, nullable=False)
-    period = sa.Column(sa.String(24))
-
-    def __repr__(self):
-        if self.every == 1:
-            return 'every {0}'.format(self.period_singular)
-        return 'every {0} {1}'.format(self.every, self.period)
-
-    @property
-    def schedule(self):
-        return schedules.schedule(
-            dt.timedelta(**{self.period: self.every}),
-            # nowfun=lambda: make_aware(now())
-            # nowfun=dt.datetime.now
-        )
-
-    @classmethod
-    def from_schedule(cls, session, schedule, period=SECONDS):
-        every = max(schedule.run_every.total_seconds(), 0)
-        model = session.query(IntervalSchedule).filter_by(
-            every=every, period=period).first()
-        if not model:
-            model = cls(every=every, period=period)
-            session.add(model)
-            session.commit()
-        return model
-
-    @property
-    def period_singular(self):
-        return self.period[:-1]
-
-
-class CrontabSchedule(ModelBase, ModelMixin):
-    __tablename__ = 'celery_crontab_schedule'
-    __table_args__ = {'sqlite_autoincrement': True}
-
-    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
-    minute = sa.Column(sa.String(60 * 4), default='*')
-    hour = sa.Column(sa.String(24 * 4), default='*')
-    day_of_week = sa.Column(sa.String(64), default='*')
-    day_of_month = sa.Column(sa.String(31 * 4), default='*')
-    month_of_year = sa.Column(sa.String(64), default='*')
-    timezone = sa.Column(sa.String(64), default='UTC')
-
-    def __repr__(self):
-        return '{0} {1} {2} {3} {4} (m/h/d/dM/MY) {5}'.format(
-            cronexp(self.minute), cronexp(self.hour),
-            cronexp(self.day_of_week), cronexp(self.day_of_month),
-            cronexp(self.month_of_year), str(self.timezone)
-        )
-
-    @property
-    def schedule(self):
-        return TzAwareCrontab(
-            minute=self.minute,
-            hour=self.hour, day_of_week=self.day_of_week,
-            day_of_month=self.day_of_month,
-            month_of_year=self.month_of_year,
-            tz=pytz.timezone(self.timezone)
-        )
-
-    @classmethod
-    def from_schedule(cls, session, schedule):
-        spec = {
-            'minute': schedule._orig_minute,
-            'hour': schedule._orig_hour,
-            'day_of_week': schedule._orig_day_of_week,
-            'day_of_month': schedule._orig_day_of_month,
-            'month_of_year': schedule._orig_month_of_year,
-        }
-        if schedule.tz:
-            spec.update({
-                'timezone': schedule.tz.zone
-            })
-        model = session.query(CrontabSchedule).filter_by(**spec).first()
-        if not model:
-            model = cls(**spec)
-            session.add(model)
-            session.commit()
-        return model
-
-
-class SolarSchedule(ModelBase, ModelMixin):
-    __tablename__ = 'celery_solar_schedule'
-    __table_args__ = {'sqlite_autoincrement': True}
-
-    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
-
-    event = sa.Column(sa.String(24))
-    latitude = sa.Column(sa.Float())
-    longitude = sa.Column(sa.Float())
-
-    @property
-    def schedule(self):
-        return schedules.solar(
-            self.event,
-            self.latitude,
-            self.longitude,
-            nowfun=dt.datetime.now
-        )
-
-    @classmethod
-    def from_schedule(cls, session, schedule):
-        spec = {
-            'event': schedule.event,
-            'latitude': schedule.lat,
-            'longitude': schedule.lon
-        }
-        model = session.query(SolarSchedule).filter_by(**spec).first()
-        if not model:
-            model = cls(**spec)
-            session.add(model)
-            session.commit()
-        return model
-
-    def __repr__(self):
-        return '{0} ({1}, {2})'.format(
-            self.event,
-            self.latitude,
-            self.longitude
-        )
-
-
-class PeriodicTaskChanged(ModelBase, ModelMixin):
-    """Helper table for tracking updates to periodic tasks."""
-
-    __tablename__ = 'celery_periodic_task_changed'
-
-    id = sa.Column(sa.Integer, primary_key=True)
-    last_update = sa.Column(
-        sa.DateTime(timezone=True), nullable=False, default=dt.datetime.now)
-
-    @classmethod
-    def changed(cls, mapper, connection, target):
-        """
-        :param mapper: the Mapper which is the target of this event
-        :param connection: the Connection being used
-        :param target: the mapped instance being persisted
-        """
-        if not target.no_changes:
-            cls.update_changed(mapper, connection, target)
-
-    @classmethod
-    def update_changed(cls, mapper, connection, target):
-        """
-        :param mapper: the Mapper which is the target of this event
-        :param connection: the Connection being used
-        :param target: the mapped instance being persisted
-        """
-        s = connection.execute(select([PeriodicTaskChanged]).
-                               where(PeriodicTaskChanged.id == 1).limit(1))
-        if not s:
-            s = connection.execute(insert(PeriodicTaskChanged),
-                                   last_update=dt.datetime.now())
-        else:
-            s = connection.execute(update(PeriodicTaskChanged).
-                                   where(PeriodicTaskChanged.id == 1).
-                                   values(last_update=dt.datetime.now()))
-
-    @classmethod
-    def last_change(cls, session):
-        periodic_tasks = session.query(PeriodicTaskChanged).get(1)
-        if periodic_tasks:
-            return periodic_tasks.last_update
-
-
-class PeriodicTask(ModelBase, ModelMixin):
-
-    __tablename__ = 'celery_periodic_task'
-    __table_args__ = {'sqlite_autoincrement': True}
-
-    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
-    # name
-    name = sa.Column(sa.String(255), unique=True)
-    # task name
-    task = sa.Column(sa.String(255))
-
-    # not use ForeignKey
-    interval_id = sa.Column(sa.Integer)
-    interval = relationship(
-        IntervalSchedule,
-        uselist=False,
-        primaryjoin=foreign(interval_id) == remote(IntervalSchedule.id)
-    )
-
-    crontab_id = sa.Column(sa.Integer)
-    crontab = relationship(
-        CrontabSchedule,
-        uselist=False,
-        primaryjoin=foreign(crontab_id) == remote(CrontabSchedule.id)
-    )
-
-    solar_id = sa.Column(sa.Integer)
-    solar = relationship(
-        SolarSchedule,
-        uselist=False,
-        primaryjoin=foreign(solar_id) == remote(SolarSchedule.id)
-    )
-
-    args = sa.Column(sa.Text(), default='[]')
-    kwargs = sa.Column(sa.Text(), default='{}')
-    # queue for celery
-    queue = sa.Column(sa.String(255))
-    # exchange for celery
-    exchange = sa.Column(sa.String(255))
-    # routing_key for celery
-    routing_key = sa.Column(sa.String(255))
-    priority = sa.Column(sa.Integer())
-    expires = sa.Column(sa.DateTime(timezone=True))
-
-    # 只执行一次
-    one_off = sa.Column(sa.Boolean(), default=False)
-    start_time = sa.Column(sa.DateTime(timezone=True))
-    enabled = sa.Column(sa.Boolean(), default=True)
-    last_run_at = sa.Column(sa.DateTime(timezone=True))
-    total_run_count = sa.Column(sa.Integer(), nullable=False, default=0)
-    # 修改时间
-    date_changed = sa.Column(sa.DateTime(timezone=True),
-                             default=func.now(), onupdate=func.now())
-    description = sa.Column(sa.Text(), default='')
-
-    no_changes = False
-
-    def __repr__(self):
-        fmt = '{0.name}: {{no schedule}}'
-        if self.interval:
-            fmt = '{0.name}: {0.interval}'
-        elif self.crontab:
-            fmt = '{0.name}: {0.crontab}'
-        elif self.solar:
-            fmt = '{0.name}: {0.solar}'
-        return fmt.format(self)
-
-    @property
-    def task_name(self):
-        return self.task
-
-    @task_name.setter
-    def task_name(self, value):
-        self.task = value
-
-    @property
-    def schedule(self):
-        if self.interval:
-            return self.interval.schedule
-        elif self.crontab:
-            return self.crontab.schedule
-        elif self.solar:
-            return self.solar.schedule
-        raise ValueError('{} schedule is None!'.format(self.name))
-
-
-listen(PeriodicTask, 'after_insert', PeriodicTaskChanged.update_changed)
-listen(PeriodicTask, 'after_delete', PeriodicTaskChanged.update_changed)
-listen(PeriodicTask, 'after_update', PeriodicTaskChanged.changed)
-listen(IntervalSchedule, 'after_insert', PeriodicTaskChanged.update_changed)
-listen(IntervalSchedule, 'after_delete', PeriodicTaskChanged.update_changed)
-listen(IntervalSchedule, 'after_update', PeriodicTaskChanged.update_changed)
-listen(CrontabSchedule, 'after_insert', PeriodicTaskChanged.update_changed)
-listen(CrontabSchedule, 'after_delete', PeriodicTaskChanged.update_changed)
-listen(CrontabSchedule, 'after_update', PeriodicTaskChanged.update_changed)
-listen(SolarSchedule, 'after_insert', PeriodicTaskChanged.update_changed)
-listen(SolarSchedule, 'after_delete', PeriodicTaskChanged.update_changed)
-listen(SolarSchedule, 'after_update', PeriodicTaskChanged.update_changed)
diff --git a/celery_sqlalchemy_scheduler/session.py b/celery_sqlalchemy_scheduler/session.py
deleted file mode 100644
index 04ec044..0000000
--- a/celery_sqlalchemy_scheduler/session.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# coding=utf-8
-"""SQLAlchemy session."""
-
-from contextlib import contextmanager
-
-from sqlalchemy import create_engine
-from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.orm import sessionmaker
-from sqlalchemy.pool import NullPool
-
-from kombu.utils.compat import register_after_fork
-
-ModelBase = declarative_base()
-
-
-@contextmanager
-def session_cleanup(session):
-    try:
-        yield
-    except Exception:
-        session.rollback()
-        raise
-    finally:
-        session.close()
-
-
-def _after_fork_cleanup_session(session):
-    session._after_fork()
-
-
-class SessionManager(object):
-    """Manage SQLAlchemy sessions."""
-
-    def __init__(self):
-        self._engines = {}
-        self._sessions = {}
-        self.forked = False
-        self.prepared = False
-        if register_after_fork is not None:
-            register_after_fork(self, _after_fork_cleanup_session)
-
-    def _after_fork(self):
-        self.forked = True
-
-    def get_engine(self, dburi, **kwargs):
-        if self.forked:
-            try:
-                return self._engines[dburi]
-            except KeyError:
-                engine = self._engines[dburi] = create_engine(dburi, **kwargs)
-                return engine
-        else:
-            return create_engine(dburi, poolclass=NullPool)
-
-    def create_session(self, dburi, short_lived_sessions=False, **kwargs):
-        engine = self.get_engine(dburi, **kwargs)
-        if self.forked:
-            if short_lived_sessions or dburi not in self._sessions:
-                self._sessions[dburi] = sessionmaker(bind=engine)
-            return engine, self._sessions[dburi]
-        else:
-            return engine, sessionmaker(bind=engine)
-
-    def prepare_models(self, engine):
-        if not self.prepared:
-            ModelBase.metadata.create_all(engine)
-            self.prepared = True
-
-    def session_factory(self, dburi, **kwargs):
-        engine, session = self.create_session(dburi, **kwargs)
-        self.prepare_models(engine)
-        return session()
diff --git a/celery_sqlalchemy_scheduler/tzcrontab.py b/celery_sqlalchemy_scheduler/tzcrontab.py
deleted file mode 100644
index b4c7a6e..0000000
--- a/celery_sqlalchemy_scheduler/tzcrontab.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# coding=utf-8
-"""Timezone aware Cron schedule Implementation."""
-
-import pytz
-from collections import namedtuple
-import datetime as dt
-
-from celery import schedules
-
-
-schedstate = namedtuple('schedstate', ('is_due', 'next'))
-
-
-class TzAwareCrontab(schedules.crontab):
-    """Timezone Aware Crontab."""
-
-    def __init__(
-            self, minute='*', hour='*', day_of_week='*',
-            day_of_month='*', month_of_year='*', tz=pytz.utc, app=None
-    ):
-        """Overwrite Crontab constructor to include a timezone argument."""
-        self.tz = tz
-
-        nowfun = self.nowfunc
-
-        super(TzAwareCrontab, self).__init__(
-            minute=minute, hour=hour, day_of_week=day_of_week,
-            day_of_month=day_of_month, month_of_year=month_of_year,
-            # tz=tz,
-            nowfun=nowfun, app=app
-        )
-
-    def nowfunc(self):
-        return self.tz.normalize(
-            pytz.utc.localize(dt.datetime.utcnow())
-        )
-
-    def is_due(self, last_run_at):
-        """Calculate when the next run will take place.
-
-        Return tuple of (is_due, next_time_to_check).
-        The last_run_at argument needs to be timezone aware.
-
-        """
-        # convert last_run_at to the schedule timezone
-        last_run_at = last_run_at.astimezone(self.tz)
-
-        rem_delta = self.remaining_estimate(last_run_at)
-        rem = max(rem_delta.total_seconds(), 0)
-        due = rem == 0
-        if due:
-            rem_delta = self.remaining_estimate(self.now())
-            rem = max(rem_delta.total_seconds(), 0)
-        return schedstate(due, rem)
-
-    # Needed to support pickling
-    def __repr__(self):
-        return """<crontab: {0._orig_minute} {0._orig_hour} \
-{0._orig_day_of_week} {0._orig_day_of_month} \
-{0._orig_month_of_year} (m/h/d/dM/MY), {0.tz}>""".format(self)
-
-    def __reduce__(self):
-        return (self.__class__, (self._orig_minute,
-                                 self._orig_hour,
-                                 self._orig_day_of_week,
-                                 self._orig_day_of_month,
-                                 self._orig_month_of_year,
-                                 self.tz), None)
-
-    def __eq__(self, other):
-        if isinstance(other, schedules.crontab):
-            return (other.month_of_year == self.month_of_year
-                    and other.day_of_month == self.day_of_month
-                    and other.day_of_week == self.day_of_week
-                    and other.hour == self.hour
-                    and other.minute == self.minute
-                    and other.tz == self.tz)
-        return NotImplemented
diff --git a/dev-requirements.in b/dev-requirements.in
new file mode 100644
index 0000000..12795cb
--- /dev/null
+++ b/dev-requirements.in
@@ -0,0 +1,20 @@
+# lint
+black
+flake8
+flynt
+isort
+yamllint
+
+# mypy
+mypy
+types-mock
+types-pytz
+sqlalchemy_utils
+
+# testing
+pytest
+coverage
+mock
+
+# version
+bumpversion
diff --git a/examples/base/tasks.py b/examples/base/tasks.py
deleted file mode 100644
index 88f1a8d..0000000
--- a/examples/base/tasks.py
+++ /dev/null
@@ -1,186 +0,0 @@
-# coding=utf-8
-"""
-Ready::
-
-    $ pipenv install
-
-Run Worker::
-
-    # console 1 , in pipenv shell
-    $ pipenv shell
-    $ cd examples/base
-
-    # Celery < 5.0
-    $ celery worker -A tasks:celery -l info
-
-    # Celery >= 5.0
-    $ celery -A tasks:celery worker -l info
-
-Run Beat::
-
-    # console 2, in pipenv shell
-    $ pipenv shell
-    $ cd examples/base
-
-    # Celery < 5.0
-    $ celery beat -A tasks:celery -S tasks:DatabaseScheduler -l info
-
-    # Celery >= 5.0
-    $ celery -A tasks:celery beat -S tasks:DatabaseScheduler -l info
-
-Console 3::
-
-    # console 3, in pipenv shell
-    $ pipenv shell
-    $ cd examples/base
-    $ python -m doctest tasks.py
-
-
->>> import json
->>> from celery_sqlalchemy_scheduler.models import PeriodicTask, IntervalSchedule
->>> from celery_sqlalchemy_scheduler.session import SessionManager
-
->>> beat_dburi = 'sqlite:///schedule.db'
->>> session_manager = SessionManager()
->>> engine, Session = session_manager.create_session(beat_dburi)
->>> session = Session()
-
-# Disable 'echo-every-3-seconds' task
->>> task = session.query(PeriodicTask).filter_by(name='echo-every-3-seconds').first()
->>> task.enabled = False
->>> session.add(task)
->>> session.commit()
-
-
->>> schedule = session.query(IntervalSchedule).filter_by(every=10, period=IntervalSchedule.SECONDS).first()
->>> if not schedule:
-...     schedule = IntervalSchedule(every=10, period=IntervalSchedule.SECONDS)
-...     session.add(schedule)
-...     session.commit()
-
-# Add 'add-every-10s' task
->>> task = PeriodicTask(
-...     interval=schedule,
-...     name='add-every-10s',
-...     task='tasks.add',  # name of task.
-...     args=json.dumps([1, 5])
-... )
->>> session.add(task)
->>> session.commit()
->>> print('Add ' + task.name)
-Add add-every-10s
-
->>> task.args=json.dumps([10, 2])
->>> session.add(task)
->>> session.commit()
-"""
-import os
-import time
-import platform
-import datetime as dt
-from datetime import timedelta
-
-from celery import Celery
-from celery import schedules
-
-from celery_sqlalchemy_scheduler.schedulers import DatabaseScheduler  # noqa
-
-# load environment variable from .env
-from dotenv import load_dotenv
-dotenv_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), '.env')
-if os.path.exists(dotenv_path):
-    load_dotenv(dotenv_path, override=True)
-
-# for and convenient to test and modify
-# 可以在 examples/base 目录下创建 .env 文件，修改对应的变量
-ECHO_EVERY_MINUTE = os.getenv('ECHO_EVERY_MINUTE', '0')
-ECHO_EVERY_HOUR = os.getenv('ECHO_EVERY_HOUR', '8')
-
-if platform.system() == 'Windows':
-    # must set the environment variable in windows for celery,
-    # or else celery maybe don't work
-    os.environ['FORKED_BY_MULTIPROCESSING'] = '1'
-
-# rabbitmq
-backend = 'rpc://'
-broker_url = 'amqp://guest:guest@127.0.0.1:5672//'
-
-
-# this scheduler will be reset after the celery beat restart
-beat_schedule = {
-    'echo-every-3-seconds': {
-        'task': 'tasks.echo',
-        'schedule': timedelta(seconds=3),
-        'args': ('hello', ),
-        'options': {
-            'expires': dt.datetime.utcnow() + timedelta(seconds=10)  # right
-            # 'expires': dt.datetime.now() + timedelta(seconds=30)  # error
-            # 'expires': 10  # right
-        }
-    },
-    'add-every-minutes': {
-        'task': 'tasks.add',
-        'schedule': schedules.crontab('*', '*', '*'),
-        'args': (1, 2)
-    },
-    'echo-every-hours': {
-        'task': 'tasks.echo',
-        'schedule': schedules.crontab(ECHO_EVERY_MINUTE, '*', '*'),
-        'args': ('echo-every-hours',)
-    },
-    'echo-every-days': {
-        'task': 'tasks.echo',
-        'schedule': schedules.crontab(ECHO_EVERY_MINUTE, ECHO_EVERY_HOUR, '*'),
-        'args': ('echo-every-days',)
-    },
-}
-
-beat_scheduler = 'celery_sqlalchemy_scheduler.schedulers:DatabaseScheduler'
-
-beat_sync_every = 0
-
-# The maximum number of seconds beat can sleep between checking the schedule.
-# default: 0
-beat_max_loop_interval = 10
-
-# configure celery_sqlalchemy_scheduler database uri
-beat_dburi = 'sqlite:///schedule.db'
-# beat_dburi = 'mysql+mysqlconnector://root:root@127.0.0.1/celery-schedule'
-
-timezone = 'Asia/Shanghai'
-
-# prevent memory leaks
-# 默认每个worker跑完10个任务后，自我销毁程序重建来释放内存
-worker_max_tasks_per_child = 10
-
-celery = Celery('tasks',
-                backend=backend,
-                broker=broker_url)
-
-config = {
-    'beat_schedule': beat_schedule,
-    # 'beat_scheduler': beat_scheduler,  # 命令行传参配置了，所以这里并不需要写死在代码里
-    'beat_max_loop_interval': beat_max_loop_interval,
-    'beat_dburi': beat_dburi,
-
-    'timezone': timezone,
-    'worker_max_tasks_per_child': worker_max_tasks_per_child
-}
-
-celery.conf.update(config)
-
-
-@celery.task
-def add(x, y):
-    return x + y
-
-
-@celery.task
-def echo(data):
-    print(data)
-
-
-if __name__ == "__main__":
-    celery.start()
-    # import doctest
-    # doctest.testmod()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..30f3fa9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,7 @@
+[tool.black]
+line-length = 100
+
+[tool.isort]
+profile = 'black'
+line_length = 100
+skip = ['venv/']
diff --git a/rdbbeat/__init__.py b/rdbbeat/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rdbbeat/controller.py b/rdbbeat/controller.py
new file mode 100644
index 0000000..deabcda
--- /dev/null
+++ b/rdbbeat/controller.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+import json
+from typing import Any, Optional
+
+from sqlalchemy import and_
+from sqlalchemy.orm import Session
+from sqlalchemy.orm.exc import NoResultFound
+
+from rdbbeat.data_models import Schedule, ScheduledTask
+from rdbbeat.db.models import CrontabSchedule, PeriodicTask
+from rdbbeat.exceptions import PeriodicTaskNotFound
+
+
+def get_crontab_schedule(session: Session, schedule: Schedule) -> CrontabSchedule:
+    crontab = (
+        session.query(CrontabSchedule)
+        .filter(
+            and_(
+                CrontabSchedule.minute == schedule.minute,
+                CrontabSchedule.hour == schedule.hour,
+                CrontabSchedule.day_of_week == schedule.day_of_week,
+                CrontabSchedule.day_of_month == schedule.day_of_month,
+                CrontabSchedule.month_of_year == schedule.month_of_year,
+                CrontabSchedule.timezone == schedule.timezone,
+            )
+        )
+        .one_or_none()
+    )
+    return crontab or CrontabSchedule(**schedule.dict())
+
+
+def schedule_task(
+    session: Session,
+    scheduled_task: ScheduledTask,
+    queue: Optional[str] = None,
+    exchange: Optional[str] = None,
+    routing_key: Optional[str] = None,
+    **kwargs: Any,
+) -> PeriodicTask:
+    """
+    Schedule a task by adding a periodic task entry.
+    """
+    crontab = get_crontab_schedule(session=session, schedule=scheduled_task.schedule)
+    task = PeriodicTask(
+        crontab=crontab,
+        name=scheduled_task.name,
+        task=scheduled_task.task,
+        kwargs=json.dumps(kwargs),
+        queue=queue,
+        exchange=exchange,
+        routing_key=routing_key,
+    )
+    session.add(task)
+
+    return task
+
+
+def update_task_enabled_status(
+    session: Session,
+    enabled_status: bool,
+    periodic_task_id: int,
+) -> PeriodicTask:
+    """
+    Update task enabled status (if task is enabled or disabled).
+    """
+    try:
+        task = session.query(PeriodicTask).get(periodic_task_id)
+        task.enabled = enabled_status
+        session.add(task)
+
+    except NoResultFound as e:
+        raise PeriodicTaskNotFound() from e
+
+    return task
+
+
+def update_task(
+    session: Session,
+    scheduled_task: ScheduledTask,
+    periodic_task_id: int,
+) -> PeriodicTask:
+    """
+    Update the details of a task including the crontab schedule
+    """
+    try:
+        task = session.query(PeriodicTask).get(periodic_task_id)
+
+        task.crontab = get_crontab_schedule(session, scheduled_task.schedule)
+        task.name = scheduled_task.name
+        task.task = scheduled_task.task
+        session.add(task)
+
+    except NoResultFound as e:
+        raise PeriodicTaskNotFound() from e
+
+    return task
+
+
+def is_crontab_used(session: Session, crontab_schedule: CrontabSchedule) -> bool:
+    schedules = session.query(PeriodicTask).filter_by(crontab=crontab_schedule).all()
+    return True if schedules else False
+
+
+def delete_task(session: Session, periodic_task_id: int) -> PeriodicTask:
+    try:
+        task = session.query(PeriodicTask).get(periodic_task_id)
+        session.delete(task)
+        session.flush()
+        if not is_crontab_used(session, task.crontab):
+            session.delete(task.crontab)
+        return task
+    except NoResultFound as e:
+        raise PeriodicTaskNotFound() from e
diff --git a/rdbbeat/data_models.py b/rdbbeat/data_models.py
new file mode 100644
index 0000000..7cf061b
--- /dev/null
+++ b/rdbbeat/data_models.py
@@ -0,0 +1,64 @@
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+from pydantic import BaseModel, validator
+
+
+class Schedule(BaseModel):
+    minute: str = "*"
+    hour: str = "*"
+    day_of_week: str = "*"
+    day_of_month: str = "*"
+    month_of_year: str = "*"
+    timezone: str = "UTC"
+
+    @validator("minute")
+    def minute_validation(cls, v: str) -> str:
+        if "*" == v:
+            return v
+        elif not v.isdigit():
+            raise ValueError(f"Minute: '{v}' is not a valid int")
+        assert int(v) >= 0 and int(v) < 60, "Minute value must range between 0 and 59"
+        return v
+
+    @validator("hour")
+    def hour_validation(cls, v: str) -> str:
+        if "*" == v:
+            return v
+        elif not v.isdigit():
+            raise ValueError(f"Hour: '{v}' is not a valid int")
+        assert int(v) >= 0 and int(v) < 24, "Hour value must range between 0 and 23"
+        return v
+
+    @validator("day_of_week")
+    def day_of_week_validation(cls, v: str) -> str:
+        if "*" == v:
+            return v
+        elif not v.isdigit():
+            raise ValueError(f"Day of week: '{v}' is not a valid int")
+        assert int(v) >= 0 and int(v) < 7, "Day of the week value must range between 0 and 6"
+        return v
+
+    @validator("day_of_month")
+    def day_of_month_validation(cls, v: str) -> str:
+        if "*" == v:
+            return v
+        elif not v.isdigit():
+            raise ValueError(f"Day of month: '{v}' is not a valid int")
+        assert int(v) > 0 and int(v) < 32, "Day of the month value must range between 1 and 31"
+        return v
+
+    @validator("month_of_year")
+    def month_of_year_validation(cls, v: str) -> str:
+        if "*" == v:
+            return v
+        elif not v.isdigit():
+            raise ValueError(f"Month: '{v}' is not a valid int")
+        assert int(v) > 0 and int(v) < 13, "Month of year value must range between 0 and 12"
+        return v
+
+
+class ScheduledTask(BaseModel):
+    name: str
+    task: str
+    schedule: Schedule
diff --git a/rdbbeat/db/__init__.py b/rdbbeat/db/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rdbbeat/db/migrations/__init__.py b/rdbbeat/db/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rdbbeat/db/migrations/env.py b/rdbbeat/db/migrations/env.py
new file mode 100644
index 0000000..0382357
--- /dev/null
+++ b/rdbbeat/db/migrations/env.py
@@ -0,0 +1,82 @@
+from logging.config import fileConfig
+
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+
+from rdbbeat.db.models import Base
+from rdbbeat.settings import DATABASE_URL
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# this will overwrite the ini-file sqlalchemy.url path
+config.set_main_option("sqlalchemy.url", DATABASE_URL)
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+# target_metadata = mymodel.Base.metadata
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+    """Run migrations in 'offline' mode.
+    This configures the context with just a URL
+    and not an Engine, though an Engine is acceptable
+    here as well.  By skipping the Engine creation
+    we don't even need a DBAPI to be available.
+    Calls to context.execute() here emit the given string to the
+    script output.
+    """
+    url = config.get_main_option("sqlalchemy.url")
+    context.configure(
+        url=url,
+        target_metadata=target_metadata,
+        version_table_schema="scheduler",
+        literal_binds=True,
+    )
+
+    with context.begin_transaction():
+        context.run_migrations()
+
+
+def run_migrations_online():
+    """Run migrations in 'online' mode.
+    In this scenario we need to create an Engine
+    and associate a connection with the context.
+    """
+    connectable = engine_from_config(
+        config.get_section(config.config_ini_section),
+        prefix="sqlalchemy.",
+        poolclass=pool.NullPool,
+    )
+
+    with connectable.connect() as connection:
+        context.configure(
+            connection=connection,
+            target_metadata=target_metadata,
+            version_table_schema="scheduler",
+        )
+
+        # First ensure we have our scheduler schema available as all of these migrations will live
+        # within this schema.
+        connection.execute("CREATE SCHEMA IF NOT EXISTS scheduler")
+
+        with context.begin_transaction():
+            context.run_migrations()
+
+
+if context.is_offline_mode():
+    run_migrations_offline()
+else:
+    run_migrations_online()
diff --git a/rdbbeat/db/migrations/script.py.mako b/rdbbeat/db/migrations/script.py.mako
new file mode 100644
index 0000000..2c01563
--- /dev/null
+++ b/rdbbeat/db/migrations/script.py.mako
@@ -0,0 +1,24 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision | comma,n}
+Create Date: ${create_date}
+
+"""
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+branch_labels = ${repr(branch_labels)}
+depends_on = ${repr(depends_on)}
+
+
+def upgrade():
+    ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+    ${downgrades if downgrades else "pass"}
diff --git a/rdbbeat/db/migrations/versions/2022_03_16_added_crontab_periodic_task_tables.py b/rdbbeat/db/migrations/versions/2022_03_16_added_crontab_periodic_task_tables.py
new file mode 100644
index 0000000..20c4caa
--- /dev/null
+++ b/rdbbeat/db/migrations/versions/2022_03_16_added_crontab_periodic_task_tables.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2018 AngelLiang
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+"""added crontab + periodic task tables
+
+Revision ID: 6cc745c3fb42
+Revises:
+Create Date: 2022-03-16 09:48:39.018910
+
+"""
+import sqlalchemy as sa
+from alembic import op
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = "6cc745c3fb42"
+down_revision = None
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "celery_crontab_schedule",
+        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
+        sa.Column("minute", sa.String(length=240), nullable=True),
+        sa.Column("hour", sa.String(length=96), nullable=True),
+        sa.Column("day_of_week", sa.String(length=64), nullable=True),
+        sa.Column("day_of_month", sa.String(length=124), nullable=True),
+        sa.Column("month_of_year", sa.String(length=64), nullable=True),
+        sa.Column("timezone", sa.String(length=64), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+        schema="scheduler",
+    )
+    op.create_table(
+        "celery_periodic_task",
+        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
+        sa.Column("name", sa.String(length=255), nullable=True),
+        sa.Column("task", sa.String(length=255), nullable=True),
+        sa.Column("crontab_id", sa.Integer(), nullable=True),
+        sa.Column("args", sa.Text(), nullable=True),
+        sa.Column("kwargs", sa.Text(), nullable=True),
+        sa.Column("queue", sa.String(length=255), nullable=True),
+        sa.Column("exchange", sa.String(length=255), nullable=True),
+        sa.Column("routing_key", sa.String(length=255), nullable=True),
+        sa.Column("priority", sa.Integer(), nullable=True),
+        sa.Column("expires", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("one_off", sa.Boolean(), nullable=True),
+        sa.Column("start_time", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("enabled", sa.Boolean(), nullable=True),
+        sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("total_run_count", sa.Integer(), nullable=False),
+        sa.Column("date_changed", sa.DateTime(timezone=True), nullable=True),
+        sa.Column("description", sa.Text(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("name"),
+        schema="scheduler",
+    )
+    op.create_table(
+        "celery_periodic_task_changed",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("last_update", sa.DateTime(timezone=True), nullable=False),
+        sa.PrimaryKeyConstraint("id"),
+        schema="scheduler",
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table("celery_periodic_task_changed", schema="scheduler")
+    op.drop_table("celery_periodic_task", schema="scheduler")
+    op.drop_table("celery_crontab_schedule", schema="scheduler")
+    # ### end Alembic commands ###
diff --git a/rdbbeat/db/migrations/versions/__init__.py b/rdbbeat/db/migrations/versions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rdbbeat/db/models.py b/rdbbeat/db/models.py
new file mode 100644
index 0000000..782bdb9
--- /dev/null
+++ b/rdbbeat/db/models.py
@@ -0,0 +1,192 @@
+# Copyright (c) 2018 AngelLiang
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+import datetime as dt
+import logging
+from typing import Any, Dict, Union
+
+import pytz
+import sqlalchemy as sa
+from celery import schedules
+from sqlalchemy import MetaData, func
+from sqlalchemy.engine import Engine
+from sqlalchemy.event import listen
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import Session, class_mapper, foreign, relationship, remote
+from sqlalchemy.sql import insert, select, update
+
+from rdbbeat.tzcrontab import TzAwareCrontab
+
+logger = logging.getLogger(__name__)
+
+Base: Any = declarative_base(metadata=MetaData(schema="scheduler"))
+
+
+def cronexp(field: str) -> str:
+    """Representation of cron expression."""
+    return field and str(field).replace(" ", "") or "*"
+
+
+class ModelMixin:
+    @classmethod
+    def create(cls, **kw: Dict) -> "ModelMixin":
+        return cls(**kw)
+
+    def update(self, **kw: Dict) -> "ModelMixin":
+        for attr, value in kw.items():
+            setattr(self, attr, value)
+        return self
+
+
+class CrontabSchedule(Base, ModelMixin):
+    __tablename__ = "celery_crontab_schedule"
+
+    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
+    minute = sa.Column(sa.String(60 * 4), default="*")
+    hour = sa.Column(sa.String(24 * 4), default="*")
+    day_of_week = sa.Column(sa.String(64), default="*")
+    day_of_month = sa.Column(sa.String(31 * 4), default="*")
+    month_of_year = sa.Column(sa.String(64), default="*")
+    timezone = sa.Column(sa.String(64), default="UTC")
+
+    @property
+    def schedule(self) -> TzAwareCrontab:
+        return TzAwareCrontab(
+            minute=self.minute,
+            hour=self.hour,
+            day_of_week=self.day_of_week,
+            day_of_month=self.day_of_month,
+            month_of_year=self.month_of_year,
+            tz=pytz.timezone(self.timezone),
+        )
+
+    @classmethod
+    def from_schedule(cls, session: Session, schedule: schedules.crontab) -> "CrontabSchedule":
+        spec = {
+            "minute": schedule._orig_minute,
+            "hour": schedule._orig_hour,
+            "day_of_week": schedule._orig_day_of_week,
+            "day_of_month": schedule._orig_day_of_month,
+            "month_of_year": schedule._orig_month_of_year,
+        }
+        if schedule.tz:
+            spec.update({"timezone": schedule.tz.zone})
+        model = session.query(CrontabSchedule).filter_by(**spec).first()
+        if not model:
+            model = cls(**spec)
+            session.add(model)
+            session.commit()
+
+        return model
+
+
+class PeriodicTaskChanged(Base, ModelMixin):
+    """Helper table for tracking updates to periodic tasks."""
+
+    __tablename__ = "celery_periodic_task_changed"
+
+    id = sa.Column(sa.Integer, primary_key=True)
+    last_update = sa.Column(sa.DateTime(timezone=True), nullable=False, default=dt.datetime.now)
+
+    @classmethod
+    def changed(
+        cls, mapper: class_mapper, connection: Engine.connect, target: "PeriodicTask"
+    ) -> None:
+        """
+        :param mapper: the Mapper which is the target of this event
+        :param connection: the Connection being used
+        :param target: the mapped instance being persisted
+        """
+        if not target.no_changes:
+            cls.update_changed(mapper, connection, target)
+
+    @classmethod
+    def update_changed(
+        cls, mapper: class_mapper, connection: Engine.connect, target: "PeriodicTask"
+    ) -> None:
+        """
+        :param mapper: the Mapper which is the target of this event
+        :param connection: the Connection being used
+        :param target: the mapped instance being persisted
+        """
+        s = connection.execute(
+            select([PeriodicTaskChanged]).where(PeriodicTaskChanged.id == 1).limit(1)
+        )
+        if not s:
+            s = connection.execute(insert(PeriodicTaskChanged), last_update=dt.datetime.now())
+        else:
+            s = connection.execute(
+                update(PeriodicTaskChanged)
+                .where(PeriodicTaskChanged.id == 1)
+                .values(last_update=dt.datetime.now())
+            )
+
+    @classmethod
+    def last_change(cls, session: Session) -> Union[dt.datetime, None]:
+        periodic_tasks = session.query(PeriodicTaskChanged).get(1)
+        if periodic_tasks:
+            return periodic_tasks.last_update
+        return None
+
+
+class PeriodicTask(Base, ModelMixin):
+    __tablename__ = "celery_periodic_task"
+
+    id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
+    # name
+    name = sa.Column(sa.String(255), unique=True)
+    # task name
+    task = sa.Column(sa.String(255))
+
+    crontab_id = sa.Column(sa.Integer)
+    crontab = relationship(
+        CrontabSchedule,
+        uselist=False,
+        primaryjoin=foreign(crontab_id) == remote(CrontabSchedule.id),
+    )
+
+    args = sa.Column(sa.Text(), default="[]")
+    kwargs = sa.Column(sa.Text(), default="{}")
+    # queue for celery
+    queue = sa.Column(sa.String(255))
+    # exchange for celery
+    exchange = sa.Column(sa.String(255))
+    # routing_key for celery
+    routing_key = sa.Column(sa.String(255))
+    priority = sa.Column(sa.Integer())
+    expires = sa.Column(sa.DateTime(timezone=True))
+
+    # Execute only once
+    one_off = sa.Column(sa.Boolean(), default=False)
+    start_time = sa.Column(sa.DateTime(timezone=True))
+    enabled = sa.Column(sa.Boolean(), default=True)
+    last_run_at = sa.Column(sa.DateTime(timezone=True))
+    total_run_count = sa.Column(sa.Integer(), nullable=False, default=0)
+    # Change the time
+    date_changed = sa.Column(sa.DateTime(timezone=True), default=func.now(), onupdate=func.now())
+    description = sa.Column(sa.Text(), default="")
+
+    no_changes = False
+
+    @property
+    def task_name(self) -> str:
+        return self.task
+
+    @task_name.setter
+    def task_name(self, value: str) -> None:
+        self.task = value
+
+    @property
+    def schedule(self) -> schedules.schedule:
+        if self.crontab:
+            return self.crontab.schedule
+        raise ValueError(f"{self.name} schedule is None!")
+
+
+listen(PeriodicTask, "after_insert", PeriodicTaskChanged.update_changed)
+listen(PeriodicTask, "after_delete", PeriodicTaskChanged.update_changed)
+listen(PeriodicTask, "after_update", PeriodicTaskChanged.changed)
+listen(CrontabSchedule, "after_insert", PeriodicTaskChanged.update_changed)
+listen(CrontabSchedule, "after_delete", PeriodicTaskChanged.update_changed)
+listen(CrontabSchedule, "after_update", PeriodicTaskChanged.update_changed)
diff --git a/rdbbeat/exceptions.py b/rdbbeat/exceptions.py
new file mode 100644
index 0000000..64f695e
--- /dev/null
+++ b/rdbbeat/exceptions.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+
+class PeriodicTaskNotFound(Exception):
+    """
+    Raised when a periodic task is not found in the database.
+    """
+
+    pass
diff --git a/rdbbeat/py.typed b/rdbbeat/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/celery_sqlalchemy_scheduler/schedulers.py b/rdbbeat/schedulers.py
similarity index 58%
rename from celery_sqlalchemy_scheduler/schedulers.py
rename to rdbbeat/schedulers.py
index ec21934..f55731e 100644
--- a/celery_sqlalchemy_scheduler/schedulers.py
+++ b/rdbbeat/schedulers.py
@@ -1,43 +1,31 @@
-# coding=utf-8
+# Copyright (c) 2018 AngelLiang
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
 
-import logging
 import datetime as dt
+import logging
 from multiprocessing.util import Finalize
+from typing import Any, Dict, List, Optional, Set, Tuple
 
 import sqlalchemy
-from celery import current_app
-from celery import schedules
-from celery.beat import Scheduler, ScheduleEntry
-from celery.utils.log import get_logger
+from celery import Celery, current_app, schedules
+from celery.beat import ScheduleEntry, Scheduler
 from celery.utils.time import maybe_make_aware
-from kombu.utils.encoding import safe_repr, safe_str
 from kombu.utils.json import dumps, loads
 
-from .session import session_cleanup
-from .session import SessionManager
-from .models import (
-    PeriodicTask, PeriodicTaskChanged,
-    CrontabSchedule, IntervalSchedule,
-    SolarSchedule,
-)
+from rdbbeat.db.models import CrontabSchedule, PeriodicTask, PeriodicTaskChanged
 
 # This scheduler must wake up more frequently than the
 # regular of 5 minutes because it needs to take external
 # changes to the schedule into account.
 DEFAULT_MAX_INTERVAL = 5  # seconds
 
-DEFAULT_BEAT_DBURI = 'sqlite:///schedule.db'
-
 ADD_ENTRY_ERROR = """\
 Cannot add entry %r to database schedule: %r. Contents: %r
 """
 
 
-session_manager = SessionManager()
-# session = session_manager()
-
-
-logger = get_logger('celery_sqlalchemy_scheduler.schedulers')
+logger = logging.getLogger(__name__)
 
 
 class ModelEntry(ScheduleEntry):
@@ -45,46 +33,49 @@ class ModelEntry(ScheduleEntry):
 
     model_schedules = (
         # (schedule_type, model_type, model_field)
-        (schedules.crontab, CrontabSchedule, 'crontab'),
-        (schedules.schedule, IntervalSchedule, 'interval'),
-        (schedules.solar, SolarSchedule, 'solar'),
+        (schedules.crontab, CrontabSchedule, "crontab"),
     )
-    save_fields = ['last_run_at', 'total_run_count', 'no_changes']
-
-    def __init__(self, model, Session, app=None, **kw):
+    save_fields = ["last_run_at", "total_run_count", "no_changes"]
+
+    def __init__(
+        self,
+        model: schedules.schedule,
+        session_scope: sqlalchemy.orm.Session,
+        app: Celery = None,
+        **kw: Any,
+    ) -> None:
         """Initialize the model entry."""
         self.app = app or current_app._get_current_object()
-        self.session = kw.get('session')
-        self.Session = Session
-
+        self.session = kw.get("session")
+        self.session_scope = session_scope
         self.model = model
         self.name = model.name
         self.task = model.task
 
         try:
             self.schedule = model.schedule
-            logger.debug('schedule: {}'.format(self.schedule))
+            logger.debug(f"schedule: {self.schedule}")
         except Exception as e:
             logger.error(e)
             logger.error(
-                'Disabling schedule %s that was removed from database',
+                "Disabling schedule %s that was removed from database",
                 self.name,
             )
             self._disable(model)
 
         try:
-            self.args = loads(model.args or '[]')
-            self.kwargs = loads(model.kwargs or '{}')
+            self.args = loads(model.args or "[]")
+            self.kwargs = loads(model.kwargs or "{}")
         except ValueError as exc:
             logger.exception(
-                'Removing schedule %s for argument deseralization error: %r',
-                self.name, exc,
+                "Removing schedule %s for argument deseralization error: %r",
+                self.name,
+                exc,
             )
             self._disable(model)
 
         self.options = {}
-        for option in ['queue', 'exchange', 'routing_key', 'expires',
-                       'priority']:
+        for option in ["queue", "exchange", "routing_key", "expires", "priority"]:
             value = getattr(model, option)
             if value is None:
                 continue
@@ -97,32 +88,21 @@ def __init__(self, model, Session, app=None, **kw):
             model.last_run_at = self._default_now()
         self.last_run_at = model.last_run_at
 
-        # 因为从数据库读取的 last_run_at 可能没有时区信息，所以这里必须加上时区信息
+        # update tzinfo since it may not be present
         self.last_run_at = self.last_run_at.replace(tzinfo=self.app.timezone)
 
-        # self.options['expires'] 同理
-        # if 'expires' in self.options:
-        #     expires = self.options['expires']
-        #     self.options['expires'] = expires.replace(tzinfo=self.app.timezone)
-
-    def _disable(self, model):
+    def _disable(self, model: schedules.schedule) -> None:
         model.no_changes = True
         self.model.enabled = self.enabled = model.enabled = False
         if self.session:
             self.session.add(model)
             self.session.commit()
         else:
-            session = self.Session()
-            with session_cleanup(session):
+            with self.session_scope() as session:
                 session.add(model)
                 session.commit()
 
-            #     obj = session.query(PeriodicTask).get(model.id)
-            #     obj.enable = model.enabled
-            #     session.add(obj)
-            #     session.commit()
-
-    def is_due(self):
+    def is_due(self) -> bool:
         if not self.model.enabled:
             # 5 second delay for re-enable.
             return schedules.schedstate(False, 5.0)
@@ -130,8 +110,7 @@ def is_due(self):
         # START DATE: only run after the `start_time`, if one exists.
         if self.model.start_time is not None:
             now = maybe_make_aware(self._default_now())
-            start_time = self.model.start_time.replace(
-                tzinfo=self.app.timezone)
+            start_time = self.model.start_time.replace(tzinfo=self.app.timezone)
             if now < start_time:
                 # The datetime is before the start date - don't run.
                 _, delay = self.schedule.is_due(self.last_run_at)
@@ -139,43 +118,41 @@ def is_due(self):
                 return schedules.schedstate(False, delay)
 
         # ONE OFF TASK: Disable one off tasks after they've ran once
-        if self.model.one_off and self.model.enabled \
-                and self.model.total_run_count > 0:
+        if self.model.one_off and self.model.enabled and self.model.total_run_count > 0:
             self.model.enabled = False  # disable
             self.model.total_run_count = 0  # Reset
             self.model.no_changes = False  # Mark the model entry as changed
-            save_fields = ('enabled',)   # the additional fields to save
+            save_fields = ("enabled",)  # the additional fields to save
             self.save(save_fields)
 
             return schedules.schedstate(False, None)  # Don't recheck
 
         return self.schedule.is_due(self.last_run_at)
 
-    def _default_now(self):
+    def _default_now(self) -> dt.datetime:
         now = self.app.now()
-        # The PyTZ datetime must be localised for the Django-Celery-Beat
-        # scheduler to work. Keep in mind that timezone arithmatic
+        # The PyTZ datetime must be localised for the scheduler to work
+        # Keep in mind that timezone arithmatic
         # with a localized timezone may be inaccurate.
         # return now.tzinfo.localize(now.replace(tzinfo=None))
         return now.replace(tzinfo=self.app.timezone)
 
-    def __next__(self):
+    def __next__(self) -> ScheduleEntry:
         # should be use `self._default_now()` or `self.app.now()` ?
         self.model.last_run_at = self.app.now()
         self.model.total_run_count += 1
         self.model.no_changes = True
-        return self.__class__(self.model, Session=self.Session)
+        return self.__class__(self.model, self.session_scope)
+
     next = __next__  # for 2to3
 
-    def save(self, fields=tuple()):
+    def save(self, fields: Tuple = tuple()) -> None:
         """
         :params fields: tuple, the additional fields to save
         """
-        # TODO:
-        session = self.Session()
-        with session_cleanup(session):
-            # Object may not be synchronized, so only
-            # change the fields we care about.
+        # Object may not be synchronized, so only
+        # change the fields we care about.
+        with self.session_scope() as session:
             obj = session.query(PeriodicTask).get(self.model.id)
 
             for field in self.save_fields:
@@ -186,19 +163,22 @@ def save(self, fields=tuple()):
             session.commit()
 
     @classmethod
-    def to_model_schedule(cls, session, schedule):
+    def to_model_schedule(
+        cls, session: sqlalchemy.orm.Session, schedule: schedules.schedule
+    ) -> Tuple[CrontabSchedule, str]:
         for schedule_type, model_type, model_field in cls.model_schedules:
             # change to schedule
             schedule = schedules.maybe_schedule(schedule)
             if isinstance(schedule, schedule_type):
-                # TODO:
-                model_schedule = model_type.from_schedule(session, schedule)
+                model_schedule = model_type.from_schedule(session, schedule)  # type: ignore
                 return model_schedule, model_field
-        raise ValueError(
-            'Cannot convert schedule type {0!r} to model'.format(schedule))
+
+        raise ValueError(f"Cannot convert schedule type {schedule!r} to model")
 
     @classmethod
-    def from_entry(cls, name, Session, app=None, **entry):
+    def from_entry(
+        cls, name: str, session_scope: sqlalchemy.orm.Session, app: Celery = None, **entry: Dict
+    ) -> "PeriodicTask":
         """
 
         **entry sample:
@@ -208,10 +188,8 @@ def from_entry(cls, name, Session, app=None, **entry):
              'options': {'expires': 43200}}
 
         """
-        session = Session()
-        with session_cleanup(session):
-            periodic_task = session.query(
-                PeriodicTask).filter_by(name=name).first()
+        with session_scope() as session:
+            periodic_task = session.query(PeriodicTask).filter_by(name=name).first()
             if not periodic_task:
                 periodic_task = PeriodicTask(name=name)
             temp = cls._unpack_fields(session, **entry)
@@ -225,13 +203,19 @@ def from_entry(cls, name, Session, app=None, **entry):
             except Exception as exc:
                 logger.error(exc)
                 session.rollback()
-            res = cls(periodic_task, app=app, Session=Session, session=session)
+            res = cls(periodic_task, app=app, session_scope=session_scope, session=session)
             return res
 
     @classmethod
-    def _unpack_fields(cls, session, schedule,
-                       args=None, kwargs=None, relative=None, options=None,
-                       **entry):
+    def _unpack_fields(
+        cls,
+        session: sqlalchemy.orm.Session,
+        schedule: schedules.schedule,
+        args: Optional[Any] = None,
+        kwargs: Optional[Dict] = None,
+        options: Optional[Dict] = None,
+        **entry: Dict,
+    ) -> Dict:
         """
 
         **entry sample:
@@ -244,22 +228,30 @@ def _unpack_fields(cls, session, schedule,
         model_schedule, model_field = cls.to_model_schedule(session, schedule)
         entry.update(
             # the model_id which to relationship
-            {model_field + '_id': model_schedule.id},
+            {model_field + "_id": model_schedule.id},
             args=dumps(args or []),
             kwargs=dumps(kwargs or {}),
-            **cls._unpack_options(**options or {})
+            **cls._unpack_options(**options or {}),
         )
         return entry
 
     @classmethod
-    def _unpack_options(cls, queue=None, exchange=None, routing_key=None,
-                        priority=None, one_off=None, expires=None, **kwargs):
+    def _unpack_options(
+        cls,
+        queue: Optional[str] = None,
+        exchange: Optional[str] = None,
+        routing_key: Optional[str] = None,
+        priority: Optional[int] = None,
+        one_off: Optional[bool] = None,
+        expires: Any = None,  # anti-pattern, 281 changes the type
+        **kwargs: Dict,
+    ) -> Dict:
         data = {
-            'queue': queue,
-            'exchange': exchange,
-            'routing_key': routing_key,
-            'priority': priority,
-            'one_off': one_off,
+            "queue": queue,
+            "exchange": exchange,
+            "routing_key": routing_key,
+            "priority": priority,
+            "one_off": one_off,
         }
         if expires:
             if isinstance(expires, int):
@@ -267,19 +259,12 @@ def _unpack_options(cls, queue=None, exchange=None, routing_key=None,
             elif isinstance(expires, dt.datetime):
                 pass
             else:
-                raise ValueError('expires value error')
-            data['expires'] = expires
+                raise ValueError("expires value error")
+            data["expires"] = expires
         return data
 
-    def __repr__(self):
-        return '<ModelEntry: {0} {1}(*{2}, **{3}) {4}>'.format(
-            safe_str(self.name), self.task, safe_repr(self.args),
-            safe_repr(self.kwargs), self.schedule,
-        )
-
 
 class DatabaseScheduler(Scheduler):
-
     Entry = ModelEntry
     Model = PeriodicTask
     Changes = PeriodicTaskChanged
@@ -289,48 +274,44 @@ class DatabaseScheduler(Scheduler):
     _initial_read = True
     _heap_invalidated = False
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
         """Initialize the database scheduler."""
-        self.app = kwargs['app']
-        self.dburi = kwargs.get('dburi') or self.app.conf.get(
-            'beat_dburi') or DEFAULT_BEAT_DBURI
-        self.engine, self.Session = session_manager.create_session(self.dburi)
-        session_manager.prepare_models(self.engine)
-
-        self._dirty = set()
+        self.app = kwargs["app"]
+        self.session_scope: sqlalchemy.Session = kwargs.get("session_scope") or self.app.conf.get(
+            "session_scope"
+        )
+        self._dirty: Set[Any] = set()
         Scheduler.__init__(self, *args, **kwargs)
         self._finalize = Finalize(self, self.sync, exitpriority=5)
-        self.max_interval = (kwargs.get('max_interval') or
-                             self.app.conf.beat_max_loop_interval or
-                             DEFAULT_MAX_INTERVAL)
+        self.max_interval = (
+            kwargs.get("max_interval")
+            or self.app.conf.beat_max_loop_interval
+            or DEFAULT_MAX_INTERVAL
+        )
 
-    def setup_schedule(self):
+    def setup_schedule(self) -> None:
         """override"""
-        logger.info('setup_schedule')
+        logger.info("setup_schedule")
         self.install_default_entries(self.schedule)
         self.update_from_dict(self.app.conf.beat_schedule)
 
-    def all_as_schedule(self):
-        # TODO:
-        session = self.Session()
-        with session_cleanup(session):
-            logger.debug('DatabaseScheduler: Fetching database schedule')
+    def all_as_schedule(self) -> Dict:
+        logger.debug("DatabaseScheduler: Fetching database schedule")
+        with self.session_scope() as session:
             # get all enabled PeriodicTask
             models = session.query(self.Model).filter_by(enabled=True).all()
             s = {}
             for model in models:
                 try:
-                    s[model.name] = self.Entry(model,
-                                               app=self.app,
-                                               Session=self.Session,
-                                               session=session)
+                    s[model.name] = self.Entry(
+                        model, app=self.app, session_scope=self.session_scope, session=session
+                    )
                 except ValueError:
                     pass
             return s
 
-    def schedule_changed(self):
-        session = self.Session()
-        with session_cleanup(session):
+    def schedule_changed(self) -> bool:
+        with self.session_scope() as session:
             changes = session.query(self.Changes).get(1)
             if not changes:
                 changes = self.Changes(id=1)
@@ -346,7 +327,7 @@ def schedule_changed(self):
                 self._last_timestamp = ts
             return False
 
-    def reserve(self, entry):
+    def reserve(self, entry: ScheduleEntry) -> ScheduleEntry:
         """override
 
         It will be called in parent class.
@@ -357,9 +338,9 @@ def reserve(self, entry):
         self._dirty.add(new_entry.name)
         return new_entry
 
-    def sync(self):
+    def sync(self) -> None:
         """override"""
-        logger.info('Writing entries...')
+        logger.info("Writing entries...")
         _tried = set()
         _failed = set()
         try:
@@ -367,21 +348,20 @@ def sync(self):
                 name = self._dirty.pop()
                 try:
                     self.schedule[name].save()  # save to database
-                    logger.debug(
-                        '{name} save to database'.format(name=name))
+                    logger.debug(f"{name} save to database")
                     _tried.add(name)
-                except (KeyError) as exc:
+                except KeyError as exc:
                     logger.error(exc)
                     _failed.add(name)
         except sqlalchemy.exc.IntegrityError as exc:
-            logger.exception('Database error while sync: %r', exc)
+            logger.exception("Database error while sync: %r", exc)
         except Exception as exc:
             logger.exception(exc)
         finally:
             # retry later, only for the failed ones
             self._dirty |= _failed
 
-    def update_from_dict(self, mapping):
+    def update_from_dict(self, mapping: Dict) -> None:
         s = {}
         for name, entry_fields in mapping.items():
             # {'task': 'celery.backend_cleanup',
@@ -389,8 +369,8 @@ def update_from_dict(self, mapping):
             #  'options': {'expires': 43200}}
             try:
                 entry = self.Entry.from_entry(
-                    name, Session=self.Session, app=self.app,
-                    **entry_fields)
+                    name, session_scope=self.session_scope, app=self.app, **entry_fields
+                )
                 if entry.model.enabled:
                     s[name] = entry
             except Exception as exc:
@@ -399,34 +379,35 @@ def update_from_dict(self, mapping):
         # update self.schedule
         self.schedule.update(s)
 
-    def install_default_entries(self, data):
-        entries = {}
+    def install_default_entries(self, data: Dict) -> None:
+        entries: Dict = {}
         if self.app.conf.result_expires:
             entries.setdefault(
-                'celery.backend_cleanup', {
-                    'task': 'celery.backend_cleanup',
-                    'schedule': schedules.crontab('0', '4', '*'),
-                    'options': {'expires': 12 * 3600},
+                "celery.backend_cleanup",
+                {
+                    "task": "celery.backend_cleanup",
+                    "schedule": schedules.crontab("0", "4", "*"),
+                    "options": {"expires": 12 * 3600},
                 },
             )
         self.update_from_dict(entries)
 
-    def schedules_equal(self, *args, **kwargs):
+    def schedules_equal(self, *args: Any, **kwargs: Dict) -> bool:
         if self._heap_invalidated:
             self._heap_invalidated = False
             return False
         return super(DatabaseScheduler, self).schedules_equal(*args, **kwargs)
 
     @property
-    def schedule(self):
+    def schedule(self) -> Scheduler:
         initial = update = False
         if self._initial_read:
-            logger.debug('DatabaseScheduler: initial read')
+            logger.debug("DatabaseScheduler: initial read")
             initial = update = True
             self._initial_read = False
         elif self.schedule_changed():
             # when you updated the `PeriodicTasks` model's `last_update` field
-            logger.info('DatabaseScheduler: Schedule changed.')
+            logger.info("DatabaseScheduler: Schedule changed.")
             update = True
 
         if update:
@@ -434,17 +415,12 @@ def schedule(self):
             self._schedule = self.all_as_schedule()
             # the schedule changed, invalidate the heap in Scheduler.tick
             if not initial:
-                self._heap = []
+                self._heap: List[Any] = []
                 self._heap_invalidated = True
             if logger.isEnabledFor(logging.DEBUG):
-                logger.debug('Current schedule:\n%s', '\n'.join(
-                    repr(entry) for entry in self._schedule.values()),
+                logger.debug(
+                    "Current schedule:\n%s",
+                    "\n".join(repr(entry) for entry in self._schedule.values()),
                 )
         # logger.debug(self._schedule)
         return self._schedule
-
-    @property
-    def info(self):
-        """override"""
-        # return infomation about Schedule
-        return '    . db -> {self.dburi}'.format(self=self)
diff --git a/rdbbeat/settings.py b/rdbbeat/settings.py
new file mode 100644
index 0000000..0d57299
--- /dev/null
+++ b/rdbbeat/settings.py
@@ -0,0 +1,9 @@
+import os
+
+from dotenv import load_dotenv
+
+load_dotenv()
+
+DATABASE_URL = os.environ.get(
+    "DATABASE_URL", "postgresql://username:password@localhost:5432/database"
+)
diff --git a/rdbbeat/tzcrontab.py b/rdbbeat/tzcrontab.py
new file mode 100644
index 0000000..993f6e9
--- /dev/null
+++ b/rdbbeat/tzcrontab.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2018 AngelLiang
+# Copyright (c) 2023 Hewlett Packard Enterprise Development LP
+# MIT License
+
+import datetime as dt
+from collections import namedtuple
+from datetime import datetime
+
+import pytz
+from celery import Celery, schedules
+from sqlalchemy_utils import TimezoneType
+
+schedstate = namedtuple("schedstate", ("is_due", "next"))
+
+
+class TzAwareCrontab(schedules.crontab):
+    """Timezone Aware Crontab."""
+
+    def __init__(
+        self,
+        minute: str = "*",
+        hour: str = "*",
+        day_of_week: str = "*",
+        day_of_month: str = "*",
+        month_of_year: str = "*",
+        tz: TimezoneType = pytz.utc,
+        app: Celery = None,
+    ) -> None:
+        """Overwrite Crontab constructor to include a timezone argument."""
+        self.tz = tz
+
+        nowfun = self.nowfunc
+
+        super(TzAwareCrontab, self).__init__(
+            minute=minute,
+            hour=hour,
+            day_of_week=day_of_week,
+            day_of_month=day_of_month,
+            month_of_year=month_of_year,
+            # tz=tz,
+            nowfun=nowfun,
+            app=app,
+        )
+
+    def nowfunc(self) -> datetime:
+        return self.tz.normalize(pytz.utc.localize(dt.datetime.utcnow()))
+
+    def is_due(self, last_run_at: datetime) -> schedstate:
+        """Calculate when the next run will take place.
+
+        Return tuple of (is_due, next_time_to_check).
+        The last_run_at argument needs to be timezone aware.
+
+        """
+        # convert last_run_at to the schedule timezone
+        last_run_at = last_run_at.astimezone(self.tz)
+
+        rem_delta = self.remaining_estimate(last_run_at)
+        rem = max(rem_delta.total_seconds(), 0)
+        due = rem == 0
+        if due:
+            rem_delta = self.remaining_estimate(self.now())
+            rem = max(rem_delta.total_seconds(), 0)
+        return schedstate(due, rem)
+
+    # Needed to support pickling
+    def __repr__(self) -> str:
+        return (
+            f"<crontab: {self._orig_minute} {self._orig_hour} "
+            f"{self._orig_day_of_week} {self._orig_day_of_month} "
+            f"{self._orig_month_of_year} (m/h/d/dM/MY), {self.tz}>"
+        )
+
+    def __reduce__(self) -> schedules.crontab:
+        return (
+            self.__class__,
+            (
+                self._orig_minute,
+                self._orig_hour,
+                self._orig_day_of_week,
+                self._orig_day_of_month,
+                self._orig_month_of_year,
+                self.tz,
+            ),
+            None,
+        )
+
+    def __eq__(self, other: schedules.crontab) -> bool:
+        if isinstance(other, schedules.crontab):
+            return (
+                other.month_of_year == self.month_of_year
+                and other.day_of_month == self.day_of_month
+                and other.day_of_week == self.day_of_week
+                and other.hour == self.hour
+                and other.minute == self.minute
+                and other.tz == self.tz
+            )
+        raise NotImplementedError
diff --git a/screenshot/sqlite.png b/screenshot/sqlite.png
deleted file mode 100644
index 1c9f96a..0000000
Binary files a/screenshot/sqlite.png and /dev/null differ
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..3bff998
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,40 @@
+[flake8]
+max-line-length = 100
+exclude = venv, migrations
+
+[isort]
+profile = black
+line_length = 100
+skip = venv/
+
+[mypy]
+disallow_untyped_defs = True
+exclude = venv
+
+[mypy-tests.*]
+disallow_untyped_defs = False
+check_untyped_defs = True
+
+[mypy-sqlalchemy.*]
+ignore_missing_imports = True
+
+[mypy-sqlalchemy_utils]
+ignore_missing_imports = True
+
+[mypy-celery.*]
+ignore_missing_imports = True
+
+[mypy-kombu.*]
+ignore_missing_imports = True
+
+[mypy-multiprocessing.util.*]
+ignore_missing_imports = True
+
+[mypy-setuptools.*]
+ignore_missing_imports = True
+
+[mypy-rdbbeat.db.migrations.*]
+ignore_errors = True
+
+[mypy-alembic.*]
+ignore_missing_imports = True
diff --git a/setup.py b/setup.py
index 4de85a0..e7f1423 100644
--- a/setup.py
+++ b/setup.py
@@ -1,86 +1,37 @@
 """
-    celery-sqlalchemy-scheduler
-    ~~~~~~~~~~~~~~
-    A Scheduler Based SQLalchemy For Celery.
-    :Copyright (c) 2018 AngelLiang
     :license: MIT, see LICENSE for more details.
 """
-from os import path
-from codecs import open
-try:
-    from setuptools import find_packages, setup
-except ImportError:
-    from distutils.core import setup, find_packages
-# To use a consistent encoding
 
-basedir = path.abspath(path.dirname(__file__))
-# Get the long description from the README file
-with open(path.join(basedir, 'README.md'), encoding='utf-8') as f:
-    long_description = f.read()
+from setuptools import find_packages, setup
 
 setup(
-    name="celery_sqlalchemy_scheduler",
-
-    # Versions should comply with PEP440.  For a discussion on single-sourcing
-    # the version across setup.py and the project code, see
-    # https://packaging.python.org/en/latest/single_source_version.html
-    version="0.3.0",
-    # The project's main homepage.
-    url="https://github.com/AngelLiang/celery-sqlalchemy-scheduler",
-    # Choose your license
-
-    license='MIT',
-
-    description="A Scheduler Based SQLalchemy For Celery",
-    long_description=long_description,
-    long_description_content_type='text/markdown',  # 长描述内容类型
-
-    platforms='any',
-    # Author details
-    author="AngelLiang",
-    author_email='yannanxiu@126.com',
-    home_page='https://github.com/AngelLiang/celery-sqlalchemy-scheduler',
-
-    # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+    name="rdbbeat",
+    python_requires=">=3.8",
+    author="Aruba UXI",
+    version="0.1.1",
+    description="A SQLAlchemy-based scheduler for celery-beat",
+    long_description=open("README.md").read(),
+    long_description_content_type="text/markdown",
+    license="MIT",
     classifiers=[
-        # How mature is this project? Common values are
-        #   3 - Alpha
-        #   4 - Beta
-        #   5 - Production/Stable
-        'Development Status :: 3 - Alpha',
-
-        # Indicate who your project is intended for
-        'Intended Audience :: Developers',
-        'Topic :: Software Development :: Libraries',
-
-        # Pick your license as you wish (should match "license" above)
-        'License :: OSI Approved :: MIT License',
-
-        # Specify the Python versions you support here. In particular, ensure
-        # that you indicate whether you support Python 2, Python 3 or both.
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.6',
+        "Intended Audience :: Developers",
+        "Operating System :: OS Independent",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3",
     ],
-
-    # What does your project relate to?
-    keywords="celery scheduler sqlalchemy beat",
-
-    # You can just specify the packages manually here if your project is
-    # simple. Or you can use find_packages().
-    packages=find_packages(exclude=['contrib', 'docs', 'tests']),
-
-    # Alternatively, if you want to distribute just a my_module.py, uncomment
-    # this:
-    #   py_modules=["my_module"],
-
-    # List run-time dependencies here.  These will be installed by pip when
-    # your project is installed. For an analysis of "install_requires" vs pip's
-    # requirements files see:
-    # https://packaging.python.org/en/latest/requirements.html
-
+    url="https://github.com/aruba-uxi/celery-sqlalchemy-scheduler",
+    packages=find_packages(exclude=["tests"]),
+    package_data={"rdbbeat": ["py.typed"]},
+    include_package_data=True,
+    zip_safe=False,
     install_requires=[
-        'celery>=4.2',
-        'sqlalchemy',
+        "celery~=5.2",
+        "sqlalchemy~=1.4",
+        "alembic",
+        "pydantic",
+        "python-dotenv",
+        "pytz",
     ],
-    zip_safe=False,
 )


Test patch:
diff --git a/.github/workflows/lint-test-code.yaml b/.github/workflows/lint-test-code.yaml
new file mode 100644
index 0000000..49505ef
--- /dev/null
+++ b/.github/workflows/lint-test-code.yaml
@@ -0,0 +1,30 @@
+name: Lint & Test Code
+
+on: [push]
+
+jobs:
+  lint-test-code:
+    name: Lint & Test Code
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v2
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.8
+
+      - name: Setup Just
+        uses: extractions/setup-just@v1
+
+      - name: Install Dependencies
+        run: just setup-dev
+
+      - name: Run Code Linting Test
+        run: just lint
+
+      - name: Run Tests
+        run: |
+          just test
+          just coverage
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..9af85bc
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,33 @@
+import pytest
+
+from rdbbeat.db.models import CrontabSchedule, PeriodicTask
+
+
+@pytest.fixture
+def scheduled_task():
+    schedule = {
+        "minute": "23",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+        "timezone": "UTC",
+    }
+    scheduled_task = {
+        "name": "task_1",
+        "task": "echo",
+        "schedule": schedule,
+    }
+
+    return scheduled_task
+
+
+@pytest.fixture
+def scheduled_task_db_object(scheduled_task):
+    task = PeriodicTask(
+        crontab=CrontabSchedule(**scheduled_task["schedule"]),
+        name=scheduled_task["name"],
+        task=scheduled_task["task"],
+    )
+
+    return task
diff --git a/tests/test_controller.py b/tests/test_controller.py
new file mode 100644
index 0000000..418cb83
--- /dev/null
+++ b/tests/test_controller.py
@@ -0,0 +1,151 @@
+import json
+from typing import Dict
+
+import pytest
+from mock import patch
+from sqlalchemy.orm.exc import NoResultFound
+
+from rdbbeat.controller import (
+    delete_task,
+    get_crontab_schedule,
+    is_crontab_used,
+    schedule_task,
+    update_task,
+    update_task_enabled_status,
+)
+from rdbbeat.data_models import Schedule, ScheduledTask
+from rdbbeat.db.models import CrontabSchedule, PeriodicTask
+from rdbbeat.exceptions import PeriodicTaskNotFound
+
+
+def test_get_new_crontab_schedule(scheduled_task):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.query(CrontabSchedule).filter().one_or_none.return_value = None
+        crontab = get_crontab_schedule(mock_session, Schedule(**scheduled_task.get("schedule")))
+        assert crontab.minute == scheduled_task.get("schedule")["minute"]
+        assert crontab.hour == scheduled_task.get("schedule")["hour"]
+        assert crontab.day_of_week == scheduled_task.get("schedule")["day_of_week"]
+        assert crontab.day_of_month == scheduled_task.get("schedule")["day_of_month"]
+        assert crontab.month_of_year == scheduled_task.get("schedule")["month_of_year"]
+        assert crontab.timezone == scheduled_task.get("schedule")["timezone"]
+
+
+def test_get_existing_crontab_schedule(scheduled_task, scheduled_task_db_object):
+    existing_crontab = scheduled_task_db_object.crontab
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.query(CrontabSchedule).filter().one_or_none.return_value = existing_crontab
+        crontab = get_crontab_schedule(mock_session, Schedule(**scheduled_task.get("schedule")))
+        assert existing_crontab == crontab
+
+
+def test_schedule_task(scheduled_task_db_object, scheduled_task):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.add.return_value = None
+        mock_session.query(CrontabSchedule).filter().one_or_none.return_value = None
+
+        actual_scheduled_task = schedule_task(mock_session, ScheduledTask.parse_obj(scheduled_task))
+
+        expected_scheduled_task = scheduled_task_db_object
+
+        assert actual_scheduled_task.name == expected_scheduled_task.name
+        assert actual_scheduled_task.task == expected_scheduled_task.task
+        assert actual_scheduled_task.schedule == expected_scheduled_task.schedule
+
+
+def test_schedule_task_kwargs(scheduled_task_db_object, scheduled_task):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.add.return_value = None
+        mock_session.query(CrontabSchedule).filter().one_or_none.return_value = None
+
+        actual_scheduled_task = schedule_task(
+            mock_session, ScheduledTask.parse_obj(scheduled_task), report_metadata_uid="some_uid"
+        )
+
+        expected_scheduled_task = scheduled_task_db_object
+
+        assert actual_scheduled_task.name == expected_scheduled_task.name
+        assert actual_scheduled_task.task == expected_scheduled_task.task
+        assert actual_scheduled_task.schedule == expected_scheduled_task.schedule
+        assert actual_scheduled_task.kwargs == json.dumps({"report_metadata_uid": "some_uid"})
+
+
+def test_update_task_enabled_status(scheduled_task_db_object):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.query(PeriodicTask).get.return_value = scheduled_task_db_object
+
+        periodic_task_id = 1
+        updated_task = update_task_enabled_status(mock_session, False, periodic_task_id)
+
+        assert updated_task.enabled is False
+
+
+def test_update_task_enabled_status_fail():
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        with pytest.raises(PeriodicTaskNotFound):
+            mock_session.query(PeriodicTask).get.side_effect = NoResultFound()
+
+            periodic_task_id = -1
+            update_task_enabled_status(mock_session, False, periodic_task_id)
+
+
+def test_update_task(scheduled_task_db_object):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.query(PeriodicTask).get.return_value = scheduled_task_db_object
+        mock_session.query(CrontabSchedule).filter().one_or_none.return_value = None
+
+        new_schedule: Dict = {
+            "minute": "24",
+            "hour": "01",
+            "day_of_week": "3",
+            "day_of_month": "24",
+            "month_of_year": "11",
+            "timezone": "UTC",
+        }
+        new_scheduled_task: Dict = {
+            "name": "task_2",
+            "task": "echo2",
+            "schedule": new_schedule,
+        }
+        new_schedule = CrontabSchedule(**new_schedule)
+
+        expected_updated_task = PeriodicTask(
+            crontab=new_schedule,
+            name=new_scheduled_task["name"],
+            task=new_scheduled_task["task"],
+        )
+
+        periodic_task_id = 1
+        actual_updated_db_task = update_task(
+            mock_session, ScheduledTask.parse_obj(new_scheduled_task), periodic_task_id
+        )
+
+        assert mock_session.query(PeriodicTask).get.call_count == 1
+        assert actual_updated_db_task.name == expected_updated_task.name
+        assert actual_updated_db_task.task == expected_updated_task.task
+        assert actual_updated_db_task.schedule == expected_updated_task.schedule
+
+
+def test_delete_task(scheduled_task_db_object):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        # Set up the mock_session
+        periodic_task_id = 1
+        mock_session.query(PeriodicTask).get.return_value = scheduled_task_db_object
+        mock_session.delete.return_value = None
+        # Delete task
+        with patch("rdbbeat.controller.is_crontab_used") as is_crontab_used:
+            is_crontab_used.return_value = False
+            actual_deleted_task = delete_task(mock_session, periodic_task_id)
+
+        expected_deleted_task = scheduled_task_db_object
+
+        assert actual_deleted_task.name == expected_deleted_task.name
+        assert actual_deleted_task.task == expected_deleted_task.task
+        assert actual_deleted_task.schedule == expected_deleted_task.schedule
+
+
+def test_is_crontab_used(scheduled_task_db_object):
+    with patch("sqlalchemy.orm.Session") as mock_session:
+        mock_session.query(PeriodicTask).filter_by().all.return_value = None
+
+        result = is_crontab_used(mock_session, scheduled_task_db_object.schedule)
+        assert result is False
diff --git a/tests/test_data_models.py b/tests/test_data_models.py
new file mode 100644
index 0000000..e1bf21b
--- /dev/null
+++ b/tests/test_data_models.py
@@ -0,0 +1,135 @@
+import pytest
+from pydantic import ValidationError
+
+from rdbbeat.data_models import Schedule
+
+
+def test_schedule_pass():
+    schedule = {
+        "minute": "23",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_minute_type():
+    schedule = {
+        "minute": "minute",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValueError, match="Minute: 'minute' is not a valid int"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_minute():
+    schedule = {
+        "minute": "200",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValidationError, match="Minute value must range between 0 and 59"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_hour_type():
+    schedule = {
+        "minute": "23",
+        "hour": "h",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValueError, match="Hour: 'h' is not a valid int"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_hour():
+    schedule = {
+        "minute": "23",
+        "hour": "0055",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValidationError, match="Hour value must range between 0 and 23"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_day_of_week_type():
+    schedule = {
+        "minute": "23",
+        "hour": "00",
+        "day_of_week": "day",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValueError, match="Day of week: 'day' is not a valid int"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_day_of_week():
+    schedule = {
+        "minute": "23",
+        "hour": "0055",
+        "day_of_week": "22",
+        "day_of_month": "23",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValidationError, match="Day of the week value must range between 0 and 6"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_day_of_month_type():
+    schedule = {
+        "minute": "23",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "day",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValueError, match="Day of month: 'day' is not a valid int"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_day_of_month():
+    schedule = {
+        "minute": "23",
+        "hour": "0055",
+        "day_of_week": "2",
+        "day_of_month": "32",
+        "month_of_year": "12",
+    }
+    with pytest.raises(ValidationError, match="Day of the month value must range between 1 and 31"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_month_type():
+    schedule = {
+        "minute": "23",
+        "hour": "00",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "month",
+    }
+    with pytest.raises(ValueError, match="Month: 'month' is not a valid int"):
+        Schedule.parse_obj(schedule)
+
+
+def test_schedule_invalid_month():
+    schedule = {
+        "minute": "23",
+        "hour": "0055",
+        "day_of_week": "2",
+        "day_of_month": "23",
+        "month_of_year": "0",
+    }
+    with pytest.raises(ValidationError, match="Month of year value must range between 0 and 12"):
+        Schedule.parse_obj(schedule)