es-user 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
es_user-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Babafemi Adigun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
es_user-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: es-user
3
+ Version: 0.0.1
4
+ Summary: user management for ESSL apps
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: Femi Adigun
8
+ Author-email: femi.adigun@myeverlasting.net
9
+ Requires-Python: >=3.9,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Internal User Management package for ESSL apps
21
+
@@ -0,0 +1 @@
1
+ # Internal User Management package for ESSL apps
File without changes
File without changes
@@ -0,0 +1,27 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from sqlalchemy import Column, DateTime
13
+ from sqlalchemy.sql import func
14
+ from sqlalchemy.sql.sqltypes import Integer,String
15
+ from es_user.db.session import Base
16
+ class Lead(Base):
17
+ __tablename__ = "leads"
18
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
19
+ created_at = Column(DateTime(), server_default=func.now())
20
+ first_name = Column(String)
21
+ last_name = Column(String)
22
+ email = Column(String, nullable=False)
23
+ phone_number = Column(String)
24
+ country = Column(String)
25
+ subject = Column(String)
26
+ message = Column(String)
27
+ updated_at = Column(DateTime(), server_default=func.now(), onupdate=func.now())
@@ -0,0 +1,27 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from datetime import datetime,date
13
+ from pydantic import BaseModel, EmailStr
14
+
15
+ class LeadBase(BaseModel):
16
+ first_name: str
17
+ last_name: str
18
+ email: EmailStr
19
+ phone_number: str | None = None
20
+ subject: str | None = None
21
+ message: str | None = None
22
+ created_at: datetime = datetime.now()
23
+ class LeadDto(LeadBase):
24
+ id: int
25
+ class LeadFilter(BaseModel):
26
+ start_date: date
27
+ end_date: date
@@ -0,0 +1,37 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ import logging
13
+ from sqlalchemy.orm import Session
14
+ from es_user.contact.schema import LeadDto,LeadBase
15
+ from es_user.contact.model import Lead
16
+ from es_user.db import pg
17
+ from datetime import datetime
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+ def make_contact_form(lead: LeadBase,db: Session) -> LeadDto:
22
+ """Submits a contact form.
23
+ Args:
24
+ lead (LeadDto): Lead form content.
25
+ db (Session): Database session.
26
+ """
27
+ try:
28
+ new_lead = pg.add_model(Lead,db, **lead.model_dump())
29
+ except Exception as e:
30
+ raise e
31
+ return LeadDto(**new_lead.__dict__)
32
+ def all_leads(db: Session) -> list[LeadDto]:
33
+ leads = db.query(Lead).all()
34
+ return [LeadDto(**lead.__dict__) for lead in leads]
35
+ def leads_by_created(start_date: datetime, end_date: datetime, db: Session) -> list[LeadDto]:
36
+ leads = db.query(Lead).filter(Lead.created_at >= start_date, Lead.created_at <= end_date).all()
37
+ return [LeadDto(**lead.__dict__) for lead in leads]
File without changes
@@ -0,0 +1,171 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from typing import Type,Any
13
+ from sqlalchemy.exc import IntegrityError,SQLAlchemyError
14
+ from sqlalchemy.orm import Session
15
+ import logging
16
+ logger = logging.getLogger(__name__)
17
+ def add_model(model_class: Type, db: Session, **kwargs):
18
+ """
19
+ Persist a SQLAlchemy model to the database.
20
+
21
+ Args:
22
+ - model_class (Type): The SQLAlchemy model class to be persisted.
23
+ - db (Session): The database session to use for persistence.
24
+ - **kwargs: Keyword arguments to pass to the model constructor.
25
+
26
+ Returns:
27
+ - bool: True if the model is persisted successfully, False otherwise.
28
+
29
+ Raises:
30
+ - IntegrityError: If the model cannot be persisted due to an integrity error.
31
+ """
32
+ try:
33
+ new_model = model_class(**kwargs)
34
+ db.add(new_model)
35
+ db.commit()
36
+ db.refresh(new_model)
37
+ return new_model
38
+ except IntegrityError as e:
39
+ db.rollback()
40
+ error_msg = f"Integrity Error when adding {model_class.__name__}: {str(e)}"
41
+ logger.error(error_msg)
42
+ raise ValueError(error_msg) from e
43
+ except SQLAlchemyError as e:
44
+ db.rollback()
45
+ error_msg = f"Database Error when adding {model_class.__name__}: {str(e)}"
46
+ logger.error(error_msg)
47
+ raise ValueError(error_msg) from e
48
+ except Exception as e:
49
+ db.rollback()
50
+ error_msg = f"Unexpected error when adding {model_class.__name__}: {str(e)}"
51
+ logger.error(error_msg)
52
+ raise ValueError(error_msg) from e
53
+ def update_model(model_class: Type, db: Session, model_id: int, **kwargs) -> Any:
54
+ """
55
+ Update a SQLAlchemy model in the database.
56
+
57
+ Args:
58
+ - model_class (Type): The SQLAlchemy model class to be updated.
59
+ - db (Session): The database session to use for updating.
60
+ - id (int): The ID of the model instance to be updated.
61
+ - **kwargs: Keyword arguments with the attributes to be updated and their new values.
62
+
63
+ Returns:
64
+ - model: The updatd model.
65
+
66
+ Raises:
67
+ - Exception: If an error occurs during the update process.
68
+ """
69
+ try:
70
+ model = db.query(model_class).get(model_id)
71
+ if not model:
72
+ raise ValueError(f"{model_class.__name__} with id {model_id} not found.")
73
+
74
+ for key, value in kwargs.items():
75
+ if hasattr(model, key) and key != 'id':
76
+ setattr(model, key, value)
77
+
78
+ db.commit()
79
+ db.refresh(model)
80
+ return model
81
+
82
+ except SQLAlchemyError as e:
83
+ logger.error(f"Error updating {model_class.__name__}: {e}")
84
+ db.rollback()
85
+ raise ValueError(f"Database error: {str(e)}")
86
+ except Exception as e:
87
+ logger.error(f"Unexpected error: {e}")
88
+ db.rollback()
89
+ raise ValueError(f"Unexpected error: {str(e)}")
90
+
91
+ def get_model(model_class: Type, db: Session, id: int) -> object:
92
+ """
93
+ Retrieve a SQLAlchemy model instance from the database by ID.
94
+
95
+ Args:
96
+ - model_class (Type): The SQLAlchemy model class to be retrieved.
97
+ - db (Session): The database session to use for retrieval.
98
+ - id (int): The ID of the model instance to be retrieved.
99
+
100
+ Returns:
101
+ - object: The retrieved model instance, or None if not found.
102
+ """
103
+ return db.query(model_class).filter_by(id=id).first()
104
+ def get_model_by_field(model_class: Type, db: Session, field: str, value: Any) -> object:
105
+ """
106
+ Retrieve a SQLAlchemy model instance from the database by a specified field and value.
107
+
108
+ Args:
109
+ - model_class (Type): The SQLAlchemy model class to be retrieved.
110
+ - db (Session): The database session to use for retrieval.
111
+ - field (str): The field name to filter by.
112
+ - value (Any): The value of the field to filter by.
113
+
114
+ Returns:
115
+ - object: The retrieved model instance, or None if not found.
116
+ """
117
+ return db.query(model_class).filter(getattr(model_class, field) == value).first()
118
+ def get_models_by_field(model_class: Type, db: Session, field: str, value: str) -> list[object]:
119
+ """
120
+ Retrieve a SQLAlchemy model instance from the database by a specific field.
121
+
122
+ Args:
123
+ - model_class (Type): The SQLAlchemy model class to be retrieved.
124
+ - db (Session): The database session to use for retrieval.
125
+ - field (str): The field to search for.
126
+ - value (str): The value to search for.
127
+
128
+ Returns:
129
+ - List[object]: A list of all instances of the specified model class that match the search criteria.
130
+ """
131
+ return db.query(model_class).filter(getattr(model_class, field) == value).all()
132
+
133
+ def get_all_models(model_class: Type, db: Session) -> list:
134
+ """
135
+ Retrieve all instances of a SQLAlchemy model from the database.
136
+
137
+ Args:
138
+ - model_class (Type): The SQLAlchemy model class to be retrieved.
139
+ - db (Session): The database session to use for retrieval.
140
+
141
+ Returns:
142
+ - list: A list of all instances of the specified model class.
143
+ """
144
+ return db.query(model_class).all()
145
+
146
+ def delete_model(model_class: Type, db: Session, id: int) -> bool:
147
+ """
148
+ Delete a SQLAlchemy model instance from the database by ID.
149
+
150
+ Args:
151
+ - model_class (Type): The SQLAlchemy model class to be deleted.
152
+ - db (Session): The database session to use for deletion.
153
+ - id (int): The ID of the model instance to be deleted.
154
+
155
+ Returns:
156
+ - bool: True if the model is deleted successfully, False otherwise.
157
+
158
+ Raises:
159
+ - Exception: If an error occurs during the deletion process.
160
+ """
161
+ try:
162
+ model = db.query(model_class).get(id)
163
+ if not model:
164
+ raise ValueError(f"{model_class.__name__} with id {id} not found.")
165
+ db.delete(model)
166
+ db.commit()
167
+ return True
168
+ except Exception as e:
169
+ db.rollback()
170
+ logger.error(f"Error: {e}")
171
+ raise ValueError(str(e)) from e
@@ -0,0 +1,17 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from es_user.util.CONSTANTS import SCHEMA
13
+ from sqlalchemy import MetaData
14
+ from sqlalchemy.orm import declarative_base
15
+
16
+ metadata = MetaData(schema=SCHEMA)
17
+ Base = declarative_base(metadata=metadata)
File without changes
@@ -0,0 +1,54 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from sqlalchemy import Column, DateTime, ForeignKey, Enum
13
+ from sqlalchemy.sql import func
14
+ from sqlalchemy.sql.sqltypes import Integer,String,Boolean
15
+ from sqlalchemy.dialects.postgresql import ARRAY
16
+ from sqlalchemy.orm import relationship
17
+ from es_user.db.session import Base
18
+ from es_user.util.enums import AccessRoleEnum,StatusEnum
19
+ from es_user.util.CONSTANTS import SCHEMA
20
+ class Address(Base):
21
+ __tablename__ = "addresses"
22
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
23
+ street = Column(String)
24
+ city = Column(String)
25
+ state = Column(String)
26
+ zip_code = Column(String)
27
+ email = Column(String)
28
+ phone_number = Column(String)
29
+ country = Column(String)
30
+
31
+
32
+ class Appuser(Base):
33
+ __tablename__ = "appusers"
34
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
35
+ first_name = Column(String)
36
+ last_name = Column(String)
37
+ country = Column(String)
38
+ email = Column(String, nullable=False, unique=True)
39
+ password = Column(String)
40
+ token = Column(String)
41
+ dp = Column(String)
42
+ is_active = Column(Boolean)
43
+ roles = Column(ARRAY(Enum(AccessRoleEnum, schema=SCHEMA)))
44
+ status = Column(Enum(StatusEnum, schema=SCHEMA))
45
+ gender = Column(String)
46
+ socialmedia = Column(String)
47
+ created_at = Column(DateTime(), server_default=func.now())
48
+ updated_at = Column(DateTime(), server_default=func.now(), onupdate=func.now())
49
+ address_id = Column(
50
+ Integer, ForeignKey("addresses.id"), nullable=True
51
+ )
52
+ address = relationship(
53
+ "Address", uselist=False, backref="user"
54
+ )
@@ -0,0 +1,69 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from datetime import datetime
13
+ from pydantic import BaseModel, EmailStr, field_validator, Field
14
+ from es_user.util.enums import AccessRoleEnum, StatusEnum
15
+ from typing import Literal
16
+ gender_type = Literal['MALE', 'FEMALE', 'OTHER']
17
+ class AddressBase(BaseModel):
18
+ street: str
19
+ city: str
20
+ state: str
21
+ zip_code: str | None = None
22
+ email: EmailStr | None = None
23
+ phone_number: str | None = None
24
+ country: str
25
+ class AddressDto(AddressBase):
26
+ id: int | None = None
27
+ class UserEmail(BaseModel):
28
+ email: EmailStr
29
+ class UserReset(UserEmail):
30
+ token: str
31
+ password: str
32
+ new_password: str | None = None
33
+ class UserPhoto(BaseModel):
34
+ file_path: str
35
+ user_id: int
36
+ class UserBase(BaseModel):
37
+ first_name: str
38
+ last_name: str
39
+ is_active: bool = False
40
+ roles: list[AccessRoleEnum]
41
+ socialmedia: str | None = None # comma separated string
42
+ gender: gender_type
43
+ token: str | None = None
44
+ email: EmailStr
45
+ status: StatusEnum = StatusEnum.NEW
46
+
47
+ class UserResponse(UserBase):
48
+ id: int
49
+ dp: str | None = None
50
+ created_at: datetime = Field(default_factory=datetime.now)
51
+
52
+ @field_validator('roles')
53
+ def validate_roles(cls, roles):
54
+ if AccessRoleEnum.GUEST in roles and AccessRoleEnum.ADMIN in roles:
55
+ raise ValueError("User cannot have roles of Guest and Admin at the same time")
56
+ return roles
57
+
58
+ class AppuserDto(UserBase):
59
+ password: str
60
+ address: AddressBase | None = None
61
+
62
+ class AppuserUpdate(UserBase):
63
+ id: int
64
+ address: AddressDto | None = None
65
+ @field_validator('roles')
66
+ def validate_roles(cls, roles):
67
+ if AccessRoleEnum.GUEST in roles and AccessRoleEnum.ADMIN in roles:
68
+ raise ValueError("User cannot have roles of Guest and Admin at the same time")
69
+ return roles
@@ -0,0 +1,342 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from sqlalchemy import desc
13
+ from sqlalchemy.orm import Session
14
+ from sqlalchemy.exc import SQLAlchemyError,IntegrityError
15
+ from pydantic import EmailStr
16
+ from es_user.user.model import Appuser, Address
17
+ from es_user.user.schema import AppuserDto, UserResponse, AppuserUpdate, UserReset, AddressDto
18
+ from es_user.util.service import encrypt_pass, verify_password
19
+ from es_user.util.enums import AccessRoleEnum, StatusEnum
20
+ from es_user.db import pg
21
+ import logging
22
+ import uuid
23
+
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+ def make_admin(db: Session, password: str, email: str) -> Appuser | None:
28
+ """Create admin user if it does not exist.
29
+ Args:
30
+ db (Session): Database session.
31
+ password (str): Admin password.
32
+ email (str): Admin email.
33
+ """
34
+ user = db.query(Appuser).filter(Appuser.email == email).first()
35
+ if user:
36
+ logger.info("Horace Admin user already exists")
37
+ return user
38
+ new_admin = AppuserDto(
39
+ first_name="Horace",
40
+ last_name="Admin",
41
+ email=email,
42
+ token="Horace@123",
43
+ password=encrypt_pass(password),
44
+ status=StatusEnum.NEW,
45
+ is_active=True,
46
+ gender="MALE",
47
+ roles=[AccessRoleEnum.ADMIN],
48
+ )
49
+ return pg.add_model(Appuser, db, **new_admin.__dict__)
50
+ async def add_user(user: AppuserDto, db: Session) -> UserResponse:
51
+ try:
52
+ if user.address:
53
+ addr = pg.add_model(Address, db, **user.address.__dict__)
54
+ user.address = addr
55
+ new_user = pg.add_model(Appuser, db, **user.__dict__)
56
+
57
+ if new_user:
58
+ return UserResponse(**new_user.__dict__)
59
+ else:
60
+ raise ValueError("Failed to add new user.")
61
+ except IntegrityError as e:
62
+ if "duplicate key value violates unique constraint" in str(e.orig):
63
+ logger.error(f"Error adding user: {e}")
64
+ raise ValueError(f"Email already exists. {user.email}")
65
+ else:
66
+ logger.error(f"Error adding user: {e}")
67
+ raise ValueError("An error occurred.")
68
+ except SQLAlchemyError as e:
69
+ logger.error(f"Error adding user: {e}")
70
+ raise ValueError(
71
+ "Database error.")
72
+ except Exception as e:
73
+ logger.error(f"Unexpected error: {e}")
74
+ raise ValueError(f"Unexpected error.{e}")
75
+
76
+ # def bulk_upload_users(contents: bytes, file_type: str, db: Session) -> int:
77
+ # """
78
+ # Uploads user data from a CSV or JSON file.
79
+
80
+ # Args:
81
+ # file (IO): The file object of the CSV or JSON file.
82
+ # file_type (str): "csv" or "json".
83
+ # db (Session): The database session object.
84
+
85
+ # Returns:
86
+ # int: The number of users successfully uploaded.
87
+ # """
88
+ # uploaded_count = 0
89
+
90
+ # try:
91
+ # if file_type == "csv":
92
+ # data = read_csv(io.BytesIO(contents), converters={"roles": lambda x: x.split(",")})
93
+ # #
94
+ # # records = data.to_dict(orient='records')
95
+ # elif file_type == "json":
96
+ # data = read_json(io.BytesIO(contents))
97
+ # # records = data.to_dict(orient='records')
98
+ # else:
99
+ # raise ValueError("Invalid file type. Supported types: csv, json")
100
+ # data["password"] = data["password"].apply(encrypt_pass)
101
+ # data["status"] = StatusEnum.NEW
102
+ # records = data.to_dict(orient='records')
103
+ # user_dtos = [AppuserDto(**row) for row in records]
104
+
105
+ # db.bulk_insert_mappings(Appuser, [user_dto.__dict__ for user_dto in user_dtos])
106
+ # db.commit()
107
+ # uploaded_count = len(records)
108
+
109
+ # except ValueError as ve:
110
+ # raise ve
111
+ # except (csv.Error, json.JSONDecodeError) as file_error:
112
+ # raise Exception(f"Error reading the file: {file_error}")
113
+ # except Exception as e:
114
+ # db.rollback()
115
+ # raise Exception(f"Error processing user data: {e}")
116
+
117
+ # return uploaded_count
118
+
119
+ def get_user_by_id(usr: int, db: Session) -> UserResponse:
120
+ try:
121
+ user = pg.get_model(Appuser, db, usr)
122
+ if not user:
123
+ raise ValueError(f"No user with id {usr}")
124
+ return UserResponse(**user.__dict__)
125
+ except Exception as e:
126
+ logger.error(f"Get user by id failed {str(e)}")
127
+ raise ValueError(f"Error getting User {usr}")
128
+ def activate_user(email: str, db: Session) -> bool:
129
+ try:
130
+ user = db.query(Appuser).filter(Appuser.email == email).first()
131
+ if not user:
132
+ raise ValueError(f"No user with email {email}")
133
+ user.is_active = not user.is_active
134
+ db.commit()
135
+ return True
136
+ except Exception as e:
137
+ db.rollback()
138
+ logger.error(f"Error activating user {email}: {str(e)}")
139
+ raise ValueError(f"Error activating user {email}: {str(e)}")
140
+ def update_user(user_update: AppuserUpdate, db: Session) -> UserResponse:
141
+ try:
142
+ existing_user = db.query(Appuser).get(user_update.id)
143
+ if not existing_user:
144
+ raise ValueError(f"User not found. {user_update.id}")
145
+
146
+ # Update the address if provided
147
+ if user_update.address:
148
+ if existing_user.address_id:
149
+ addr = db.query(Address).get(existing_user.address_id)
150
+ for key, value in user_update.address.__dict__.items():
151
+ setattr(addr, key, value)
152
+ addr.id = existing_user.address_id
153
+ db.commit()
154
+ db.refresh(addr)
155
+ else:
156
+ addr = Address(**user_update.address.__dict__)
157
+ db.add(addr)
158
+ db.commit()
159
+ db.refresh(addr)
160
+ existing_user.address_id = addr.id
161
+
162
+ # Update the user details
163
+ for key, value in user_update.__dict__.items():
164
+ if key != 'address' and key != 'id':
165
+ setattr(existing_user, key, value)
166
+ db.commit()
167
+ db.refresh(existing_user)
168
+
169
+ logger.info(f"User {existing_user.email} updated successfully")
170
+ return UserResponse(**existing_user.__dict__)
171
+
172
+ except IntegrityError as e:
173
+ db.rollback()
174
+ if "duplicate key value violates unique constraint" in str(e.orig):
175
+ logger.error(f"Error updating user: {e}")
176
+ raise ValueError(f"Email already exists: {user_update.email}")
177
+ else:
178
+ logger.error(f"Integrity error updating user: {e}")
179
+ raise ValueError("An error occurred during data integrity validation.")
180
+ except SQLAlchemyError as e:
181
+ db.rollback()
182
+ logger.error(f"Database error updating user: {e}")
183
+ raise ValueError("Database error.")
184
+ except Exception as e:
185
+ logger.error(f"Unexpected error: {e}")
186
+ raise ValueError(f"Unexpected error: {e}")
187
+
188
+ def update_photo(user_id: int, photo: str, db: Session) -> bool:
189
+ try:
190
+ user = db.query(Appuser).get(user_id)
191
+ user.dp = photo
192
+ db.commit()
193
+ return True
194
+ except Exception as e:
195
+ raise e
196
+
197
+ def get_userby_username(email: str,db:Session) -> Appuser:
198
+ user = db.query(Appuser).filter(Appuser.email == email).first()
199
+ return user
200
+
201
+
202
+ def all_users(db: Session) -> list[UserResponse]:
203
+ try:
204
+ users = pg.get_all_models(Appuser, db)
205
+ users_resp = [
206
+ UserResponse(
207
+ **user.__dict__
208
+ )
209
+ for user in users
210
+ ]
211
+ return users_resp
212
+ except SQLAlchemyError as e:
213
+ logger.error(f"Database error retrieving all users: {e}")
214
+ raise ValueError("Database error.")
215
+ except Exception as e:
216
+ logger.error(f"Unexpected error retrieving all users: {e}")
217
+ raise ValueError("Unexpected error.")
218
+ def paginated_users(db: Session, page: int, size: int) -> list[UserResponse]:
219
+ try:
220
+ if page < 1:
221
+ raise ValueError("Page must be greater than or equal to 1.")
222
+ if size < 1:
223
+ raise ValueError("Size must be greater than or equal to 1.")
224
+
225
+ offset = (page - 1) * size
226
+ users = db.query(Appuser).order_by(desc(Appuser.id)).offset(offset).limit(size).all()
227
+ users_resp = [
228
+ UserResponse(
229
+ **user.__dict__
230
+ )
231
+ for user in users
232
+ ]
233
+ return users_resp
234
+ except SQLAlchemyError as e:
235
+ logger.error(f"Database error retrieving paginated users: {e}")
236
+ raise ValueError("Database error.")
237
+ except ValueError:
238
+ raise
239
+ except Exception as e:
240
+ logger.error(f"Unexpected error retrieving paginated users: {e}")
241
+ raise ValueError("Unexpected error.")
242
+ def delete_user(db: Session, id: int) -> bool:
243
+ try:
244
+ user = db.query(Appuser).get(id)
245
+ if user is None:
246
+ raise ValueError(f"User with id {id} not found.")
247
+ db.delete(user)
248
+ db.commit()
249
+ return True
250
+ except Exception as e:
251
+ raise ValueError(f"An error occurred. {e}")
252
+ def set_delete_user(db: Session, id: int) -> bool:
253
+ try:
254
+ user = db.query(Appuser).get(id)
255
+ if user is None:
256
+ raise ValueError(f"User with id {id} not found.")
257
+
258
+ user.status = StatusEnum.DELETED
259
+ db.commit()
260
+ return True
261
+
262
+ except Exception as e:
263
+ db.rollback() # Rollback in case of an error
264
+ logger.error(f"Error deleting user with id {id}: {e}")
265
+ raise ValueError(f"An error occurred deleting user with id {id}.")
266
+
267
+
268
+ def users_by_role(db: Session, role: AccessRoleEnum) -> list[UserResponse]:
269
+ """
270
+ Retrieve users based on their role.
271
+
272
+ Args:
273
+ db (Session): Database session.
274
+ role (str): Role to filter users.
275
+
276
+ Returns:
277
+ List[UserResponse]: List of users matching the specified role.
278
+ """
279
+ users = db.query(Appuser).filter(Appuser.roles.contains([role])).order_by(desc(Appuser.id)).all()
280
+ return [UserResponse(**user.__dict__) for user in users]
281
+
282
+
283
+ def is_exist(email: EmailStr, db: Session) -> int:
284
+ user = db.query(Appuser).filter(Appuser.email == email).first()
285
+ return user.id if user else 0
286
+
287
+ async def rest_password_token(email: str, db: Session) -> Appuser | None:
288
+ try:
289
+ user = db.query(Appuser).filter(Appuser.email == email).first()
290
+ if not user:
291
+ raise ValueError(f"No user with email {email}")
292
+ token = str(uuid.uuid4())[:8]
293
+ user.token = token
294
+ db.commit()
295
+ # mailer=MailerDto(
296
+ # recipients=[email],
297
+ # subject="Password Reset",
298
+ # message=f"Your password reset token is {token}",
299
+ # create_at=datetime.now(),
300
+ # is_html=False
301
+ # )
302
+ # asyncio.create_task(send_email_sendgrid(mailer))
303
+ # logger.info("Password reset token %s sent to %s", token, email)
304
+ return user
305
+ except Exception as e:
306
+ db.rollback()
307
+ logger.error(f"Error resetting password for {email}: {str(e)}")
308
+ def reset_password(reset: UserReset, db: Session) -> bool:
309
+ try:
310
+ email = reset.email
311
+ user = db.query(Appuser).filter(
312
+ (Appuser.email == email) & (Appuser.token == reset.token)
313
+ ).first()
314
+ logger.info("compare %s and ",user.token, reset.token)
315
+ if not user:
316
+ raise ValueError(f"No user with email {email} or token {reset.token}")
317
+ user.password = encrypt_pass(reset.password)
318
+ db.commit()
319
+ return True
320
+ except Exception as e:
321
+ db.rollback()
322
+ logger.error(f"Error resetting password for: {str(e)}")
323
+ return False
324
+ def manual_change_password(user: UserReset, db: Session) -> bool:
325
+ try:
326
+ exist_user = db.query(Appuser).filter(Appuser.email == user.email).first()
327
+ if not exist_user:
328
+ raise ValueError(f"No user with email {user.email}")
329
+ if not verify_password(user.password, exist_user.password):
330
+ raise ValueError("Incorrect username or password") from None
331
+ if user.new_password is None:
332
+ raise ValueError("New password cannot be None")
333
+ exist_user.password = encrypt_pass(user.new_password)
334
+ db.commit()
335
+ return True
336
+ except Exception as e:
337
+ db.rollback()
338
+ logger.error(f"Error resetting password for {user.email}: {str(e)}")
339
+ return False
340
+ def create_address(address: AddressDto, db: Session) -> bool:
341
+ address = pg.add_model(Address,db, **address.model_dump())
342
+ return address
@@ -0,0 +1,18 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ from dotenv import load_dotenv
13
+ import os
14
+
15
+ load_dotenv()
16
+ SCHEMA = os.getenv("db_schema")
17
+ APP = os.getenv("app_name")
18
+ LOGO=os.getenv("logo")
File without changes
@@ -0,0 +1,30 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ import enum
13
+ class AccessRoleEnum(enum.Enum):
14
+ ADMIN = "ADMIN"
15
+ USER = "USER"
16
+ GUEST = "GUEST"
17
+ ACCOUNT = "ACCOUNT"
18
+ REFEREE = "REFEREE"
19
+ STAFF = "STAFF"
20
+ class StatusEnum(enum.Enum):
21
+ NEW = "NEW"
22
+ ENROLLED = "ENROLLED"
23
+ ADMITTED = "ADMITTED"
24
+ DEBTOR = "DEBTOR"
25
+ DELETED = "DELETED"
26
+ SUSPENDED = "SUSPENDED"
27
+ EXPELLED = "EXPELLED"
28
+ PENDING = "PENDING"
29
+ COMPLETED = "COMPLETED"
30
+ FAILED = "FAILED"
@@ -0,0 +1,23 @@
1
+ """Copyright 2024 Everlasting Systems and Solutions LLC (www.myeverlasting.net).
2
+ All Rights Reserved.
3
+
4
+ No part of this software or any of its contents may be reproduced, copied, modified or adapted, without the prior written consent of the author, unless otherwise indicated for stand-alone materials.
5
+
6
+ For permission requests, write to the publisher at the email address below:
7
+ office@myeverlasting.net
8
+
9
+ 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.
10
+
11
+ """
12
+ import bcrypt
13
+
14
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
15
+ """
16
+ Verify a plain password against the hashed version.
17
+ """
18
+ # Ensure both plain_password and hashed_password are bytes
19
+ return bcrypt.checkpw(plain_password.encode(), hashed_password.encode())
20
+ def encrypt_pass(plain_password: str) -> str:
21
+ """Hash a password for storing."""
22
+ salt = bcrypt.gensalt()
23
+ return bcrypt.hashpw(plain_password.encode(), salt).decode()
@@ -0,0 +1,15 @@
1
+ [tool.poetry]
2
+ name = "es-user"
3
+ version = "0.0.1"
4
+ description = "user management for ESSL apps"
5
+ authors = ["Femi Adigun <femi.adigun@myeverlasting.net>"]
6
+ license = "mit"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.9"
11
+
12
+
13
+ [build-system]
14
+ requires = ["poetry-core"]
15
+ build-backend = "poetry.core.masonry.api"