diff --git a/.github/workflows/test_pipeline.yml b/.github/workflows/test_pipeline.yml new file mode 100644 index 0000000..734e895 --- /dev/null +++ b/.github/workflows/test_pipeline.yml @@ -0,0 +1,134 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Devops OrmAClimate + +on: + push: + branches: [ "stage" ] + tags: + - 'v*' + + +permissions: + contents: read + +jobs: + +# ------- START ORM PROCCESS -------- # + + TestModels: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v3 + with: + python-version: "3.9" + - name: Create environment + run: | + python -m venv env + - name: Active environment + run: | + source env/bin/activate + - name: Install dependencies + run: | + pip install -r ./requirements.txt + - name: Run Tests + run: | + python -m unittest discover -s ./src/tests/ -p 'test_*.py' +# ------- END ORM PROCCESS -------- # + +# ------- START MERGE PROCCESS -------- # + + MergeMainModels: + needs: [TestModels] + name: Merge Stage with Main + permissions: write-all + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - name: Merge stage -> main + uses: devmasx/merge-branch@master + with: + type: now + head_to_merge: ${{ github.ref }} + target_branch: main + github_token: ${{ github.token }} + +# ------- END MERGE PROCCESS -------- # + +# ------- START RELEASE PROCCESS -------- # + + PostRelease: + needs: MergeMainModels + name: Create Release + runs-on: ubuntu-latest + permissions: write-all + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: '0' + # API Zip + - name: Zip artifact for deployment + run: zip releaseORM.zip ./src/* -r + # Upload Artifacts + - name: Upload Model artifact for deployment job + uses: actions/upload-artifact@v3 + with: + name: ORM + path: releaseORM.zip + # Generate Tagname + - name: Generate Tagname for release + id: taggerDryRun + uses: anothrNick/github-tag-action@1.61.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WITH_V: true + DRY_RUN: true + DEFAULT_BUMP: patch + RELEASE_BRANCHES : stage,main + BRANCH_HISTORY: last + # Create release + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + tag_name: ${{ steps.taggerDryRun.outputs.new_tag }} + release_name: Release ${{ steps.taggerDryRun.outputs.new_tag }} + #body_path: ./body.md + body: ${{ github.event.head_commit.message }} + draft: false + prerelease: false + # Upload Assets to release + - name: Upload Release Asset Model + id: upload-orm-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./releaseORM.zip + asset_name: releaseORM.zip + asset_content_type: application/zip + # update version setup.py + - name: Checkout code + uses: actions/checkout@v3 + with: + ref: main + - name: Update version + run: | + sed -i "s/version='.*'/version='${{ steps.taggerDryRun.outputs.new_tag }}'/" setup.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "Update version to ${{ steps.taggerDryRun.outputs.new_tag }}" + +# ------- END RELEASE PROCCESS -------- # \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ec8f4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,169 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +env +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +src/unittests.py + +# Folder to test +data/ +env +#data/*.* +#!data/readme.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a204ab2 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# ORM ACLIMATE + +![GitHub release (latest by date)](https://img.shields.io/github/v/release/CIAT-DAPA/aclimate_orm) ![](https://img.shields.io/github/v/tag/CIAT-DAPA/aclimate_orm) + +## Features + +- Built using Mongoengine for MongoDB +- Supports Python 3.x + +## Getting Started + +To use this Models, it is necessary to have an instance of MongoDB running. + +### Prerequisites + +- Python 3.x +- MongoDB + +## Usage + +This ORM can be used as a library in other Python projects. The models are located in the my_orm/models folder, and can be imported like any other Python module. To install this orm as a library you need to execute the following command: + +````bash +pip install git+https://github.com/CIAT-DAPA/aclimate_orm +```` + +If you want to download a specific version of orm you can do so by indicating the version tag (@v0.0.0) at the end of the install command + +````bash +pip install git+https://github.com/CIAT-DAPA/aclimate_orm@v0.2.0 +```` + +## Test +````bash +python -m unittest discover -s ./src/tests/ -p 'test_*.py' +```` + +## Models + +### Users + +Represents a User in the database. + +Attributes: + +- id: `ObjectId` - Id of the user. +- Username: `str` - User's username. +- NormalizedUserName: `str` - Normalized username. +- Email: `str` - User's email address. +- NormalizedEmail: `str` - Normalized email address. +- EmailConfirmed: `bool` - Indicates whether the user's email has been confirmed. Default is False. +- PasswordHash: `str` - Hashed password for the user. +- SecurityStamp: `str` - A random value that should change whenever a user's credentials have changed. +- ConcurrencyStamp: `str` - A value used for optimistic concurrency, ensuring updates are not based on stale data. +- PhoneNumber: `str` - User's phone number. +- PhoneNumberConfirmed: `bool` - Indicates whether the user's phone number has been confirmed. Default is False. +- TwoFactorEnabled: `bool` - Indicates whether two-factor authentication is enabled for the user. Default is False. +- LockoutEnd: `str` - Date and time in string format when the user's lockout period will end. +- LockoutEnabled: `bool` - Indicates whether lockout is enabled for the user. Default is True. +- AccessFailedCount: `int` - Number of failed access attempts. +- AuthenticatorKey: `str` - Key used for two-factor authentication. +- Roles: `List` - List of roles assigned to the user. +- Claims: `List` - List of claims associated with the user. +- Logins: `List` - List of external logins linked to the user. +- Tokens: `List` - List of tokens associated with the user. +- RecoveryCodes: `List` - List of recovery codes for two-factor authentication recovery. + +Methods: + +- `save()`: Saves the User object to the database. +- `delete()`: Deletes the User object from the database. + +### Roles + +Represents a Rol entry in the database. + +Attributes: +- _id: `StringField` - Id of the user. (Primary Key) +- Name: `StringField` - User's name. (Required) +- NormalizedName: `StringField` - Normalized name. +- ConcurrencyStamp: `StringField` - A value used for optimistic concurrency, ensuring updates are not based on stale data. (Required) + +Methods: + +- `save()`: Saves the Rol object to the database. +- `delete()`: Deletes the Rol object from the database. + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..43507fa --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +dnspython==2.4.2 +mongoengine==0.27.0 +mongomock==4.1.2 +packaging==23.2 +pymongo==4.6.0 +sentinels==1.0.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ab40231 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup, find_packages + +setup( + name="aclimate_orm", + version='0.0.0', + author="stevensotelo", + author_email="h.sotelo@cgiar.com", + description="orm for aclimate", + url="https://github.com/CIAT-DAPA/aclimate_orm", + download_url="https://github.com/CIAT-DAPA/aclimate_orm", + packages=find_packages('src'), + package_dir={'': 'src'}, + keywords='mongodb orm aclimate', + classifiers=[ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + ], + python_requires=">=3.6", + install_requires=[ + "mongoengine==0.26.0" + ] +) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c40d498 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,2 @@ +from .aclimate_orm.models import Users +from aclimate_orm.models import Roles \ No newline at end of file diff --git a/src/aclimate_orm/__init__.py b/src/aclimate_orm/__init__.py new file mode 100644 index 0000000..167a2e7 --- /dev/null +++ b/src/aclimate_orm/__init__.py @@ -0,0 +1,2 @@ +from .models.users import Users +from .models.roles import Roles \ No newline at end of file diff --git a/src/aclimate_orm/models/__init__.py b/src/aclimate_orm/models/__init__.py new file mode 100644 index 0000000..1615b11 --- /dev/null +++ b/src/aclimate_orm/models/__init__.py @@ -0,0 +1,2 @@ +from .users import Users +from .roles import Roles diff --git a/src/aclimate_orm/models/roles.py b/src/aclimate_orm/models/roles.py new file mode 100644 index 0000000..0bc8976 --- /dev/null +++ b/src/aclimate_orm/models/roles.py @@ -0,0 +1,34 @@ +from mongoengine import Document, StringField +import uuid + +class Roles(Document): + """ + Represents the Users in the database. + + Attributes: + ---------- + _id: str + Id of User. (Primary key) Required. + Name: str + Username of Rol. Required. + NormalizedName: str + Normalized Name of Rol. Optional. + ConcurrencyStamp: str + Attribute used to implement concurrency control. Required. + + Methods: + ------- + save() + Saves the Rol object to the database. + delete() + Deletes the Rol object from the database. + """ + meta = { + 'collection': 'Roles' + } + _id = StringField(primary_key=True, default=str(uuid.uuid4())) + Name = StringField(required=True) + NormalizedName = StringField() + ConcurrencyStamp = StringField(required=True) + + diff --git a/src/aclimate_orm/models/users.py b/src/aclimate_orm/models/users.py new file mode 100644 index 0000000..eca7eac --- /dev/null +++ b/src/aclimate_orm/models/users.py @@ -0,0 +1,85 @@ +from mongoengine import Document, StringField, IntField, BooleanField, ListField +import uuid + +class Users(Document): + """ + Represents the Users in the database. + + Attributes: + ---------- + _id: str + Id of User. (Primary key) Required. + UserName: str + Username of User. Required. + NormalizedUserName: str + Normalized username of User. Optional. + Email: str + Email address of User. Required. + NormalizedEmail: str + Normalized email address of User. Optional. + EmailConfirmed: bool + Indicates whether the email address has been confirmed. Defaults to False. + PasswordHash: str + Hashed password of User. Optional. + SecurityStamp: str + Security stamp used to invalidate cached security information. Optional. + ConcurrencyStamp: str + Concurrency stamp for optimistic concurrency control. Optional. + PhoneNumber: str + Phone number of User. Optional. + PhoneNumberConfirmed: bool + Indicates whether the phone number has been confirmed. Defaults to False. + TwoFactorEnabled: bool + Indicates whether two-factor authentication is enabled. Defaults to False. + LockoutEnd: str + Date and time when the lockout period ends, formatted as a string. Optional. + LockoutEnabled: bool + Indicates whether lockout is enabled. Defaults to True. + AccessFailedCount: int + Number of failed access attempts. Defaults to 0. + AuthenticatorKey: str + Authenticator key used for two-factor authentication. Optional. + Roles: list of str + List of roles assigned to the user. + Claims: list of str + List of claims associated with the user. + Logins: list of str + List of external logins linked to the user. + Tokens: list of str + List of tokens associated with the user. + RecoveryCodes: list of str + List of recovery codes for two-factor authentication recovery. + + Methods: + ------- + save() + Saves the User object to the database. + delete() + Deletes the User object from the database. + """ + meta = { + 'collection': 'Users' + } + _id = StringField(primary_key=True, default=str(uuid.uuid4())) + UserName = StringField(required=True) + NormalizedUserName = StringField() + Email = StringField(required=True) + NormalizedEmail = StringField() + EmailConfirmed = BooleanField(default=False) + PasswordHash = StringField() + SecurityStamp = StringField() + ConcurrencyStamp = StringField() + PhoneNumber = StringField() + PhoneNumberConfirmed = BooleanField(default=False) + TwoFactorEnabled = BooleanField(default=False) + LockoutEnd = StringField() + LockoutEnabled = BooleanField(default=True) + AccessFailedCount = IntField(default=0) + AuthenticatorKey = StringField() + Roles = ListField() + Claims = ListField() + Logins = ListField() + Tokens = ListField() + RecoveryCodes = ListField() + + diff --git a/src/tests/test_roles.py b/src/tests/test_roles.py new file mode 100644 index 0000000..2d8d3b0 --- /dev/null +++ b/src/tests/test_roles.py @@ -0,0 +1,40 @@ +import sys +import unittest +import os +import mongomock +from mongoengine import * +dir_path = os.path.dirname(os.path.realpath(__file__)) +orm_dir_path = os.path.abspath(os.path.join(dir_path, '..')) +sys.path.append(orm_dir_path) +from aclimate_orm.models.roles import Roles + +class TestRol(unittest.TestCase): + + def setUp(self): + disconnect() + connect('mongoenginetest', host='mongodb://localhost', mongo_client_class=mongomock.MongoClient) + + + self.rol = Roles( + Name='name', + NormalizedName='NormalizedName', + ConcurrencyStamp='9d2c6cbe-f6e9-42d2-b7ae-d4a8f65c4d96', + ) + + def tearDown(self): + + self.rol.delete() + def test_create_rol(self): + + self.rol.save() + self.assertIsNotNone(self.rol._id) + + + rol = Roles.objects(_id=self.rol._id).first() + self.assertEqual(rol.Name, 'name') + self.assertEqual(rol.NormalizedName, 'NormalizedName') + self.assertEqual(rol.ConcurrencyStamp, '9d2c6cbe-f6e9-42d2-b7ae-d4a8f65c4d96') + #coment for merge + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/src/tests/test_users.py b/src/tests/test_users.py new file mode 100644 index 0000000..5204330 --- /dev/null +++ b/src/tests/test_users.py @@ -0,0 +1,73 @@ +import sys +import unittest +import os +import mongomock +from mongoengine import * +dir_path = os.path.dirname(os.path.realpath(__file__)) +orm_dir_path = os.path.abspath(os.path.join(dir_path, '..')) +sys.path.append(orm_dir_path) +from aclimate_orm.models.users import Users + +class TestUser(unittest.TestCase): + + def setUp(self): + disconnect() + connect('mongoenginetest', host='mongodb://localhost', mongo_client_class=mongomock.MongoClient) + + + self.user = Users( + UserName='name', + NormalizedUserName='Normalized', + Email='email@mail.com', + NormalizedEmail='normalized@email.com', + EmailConfirmed=True, + PasswordHash='hashed_password', + SecurityStamp='security_stamp', + ConcurrencyStamp='concurrency_stamp', + PhoneNumber='123456789', + PhoneNumberConfirmed=True, + TwoFactorEnabled=True, + LockoutEnd='2023-12-31T00:00:00', + LockoutEnabled=True, + AccessFailedCount=2, + AuthenticatorKey='authenticator_key', + Roles=['role1', 'role2'], + Claims=['claim1', 'claim2'], + Logins=['login1', 'login2'], + Tokens=['token1', 'token2'], + RecoveryCodes=['recovery_code1', 'recovery_code2'] + ) + + def tearDown(self): + + self.user.delete() + def test_create_user(self): + + self.user.save() + self.assertIsNotNone(self.user._id) + + + user = Users.objects(_id=self.user._id).first() + self.assertEqual(user.UserName, 'name') + self.assertEqual(user.NormalizedUserName, 'Normalized') + self.assertEqual(user.Email, 'email@mail.com') + self.assertEqual(user.NormalizedEmail, 'normalized@email.com') + self.assertTrue(user.EmailConfirmed) + self.assertEqual(user.PasswordHash, 'hashed_password') + self.assertEqual(user.SecurityStamp, 'security_stamp') + self.assertEqual(user.ConcurrencyStamp, 'concurrency_stamp') + self.assertEqual(user.PhoneNumber, '123456789') + self.assertTrue(user.PhoneNumberConfirmed) + self.assertTrue(user.TwoFactorEnabled) + self.assertEqual(user.LockoutEnd, '2023-12-31T00:00:00') + self.assertTrue(user.LockoutEnabled) + self.assertEqual(user.AccessFailedCount, 2) + self.assertEqual(user.AuthenticatorKey, 'authenticator_key') + self.assertListEqual(user.Roles, ['role1', 'role2']) + self.assertListEqual(user.Claims, ['claim1', 'claim2']) + self.assertListEqual(user.Logins, ['login1', 'login2']) + self.assertListEqual(user.Tokens, ['token1', 'token2']) + self.assertListEqual(user.RecoveryCodes, ['recovery_code1', 'recovery_code2']) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file