es-user 0.1.0__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.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 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.1.0/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.1
2
+ Name: es-user
3
+ Version: 0.1.0
4
+ Summary: user management for ESSL apps
5
+ License: MIT
6
+ Author: Femi Adigun
7
+ Author-email: femi.adigun@myeverlasting.net
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Internal User Management package for ESSL apps
18
+
@@ -0,0 +1 @@
1
+ # Internal User Management package for ESSL apps
Binary file
File without changes
File without changes
@@ -0,0 +1,28 @@
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
+ created_at = Column(DateTime(), server_default=func.now())
28
+ 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 LeadFiter(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) -> bool:
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
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 = 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,334 @@
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 == "office@horacelearning.com").first()
35
+ if user:
36
+ logger.info("Horace Admin user already exists")
37
+ return
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
+ pg.add_model(Appuser, db, **new_admin.__dict__)
50
+ async def add_user(user: AppuserDto, db: Session) -> UserResponse:
51
+ try:
52
+ new_user = None
53
+ if user.address:
54
+ addr = pg.add_model(Address, db, **user.address.__dict__)
55
+ user.address = addr
56
+ new_user = pg.add_model(Appuser, db, **user.__dict__)
57
+
58
+ if new_user:
59
+ return UserResponse(**new_user.__dict__)
60
+ else:
61
+ raise ValueError("Failed to add new user.")
62
+ except IntegrityError as e:
63
+ if "duplicate key value violates unique constraint" in str(e.orig):
64
+ logger.error(f"Error adding user: {e}")
65
+ raise ValueError(f"Email already exists. {user.email}")
66
+ else:
67
+ logger.error(f"Error adding user: {e}")
68
+ raise ValueError("An error occurred.")
69
+ except SQLAlchemyError as e:
70
+ logger.error(f"Error adding user: {e}")
71
+ raise ValueError(
72
+ "Database error.")
73
+ except Exception as e:
74
+ logger.error(f"Unexpected error: {e}")
75
+ raise ValueError(f"Unexpected error.{e}")
76
+
77
+ # def bulk_upload_users(contents: bytes, file_type: str, db: Session) -> int:
78
+ # """
79
+ # Uploads user data from a CSV or JSON file.
80
+
81
+ # Args:
82
+ # file (IO): The file object of the CSV or JSON file.
83
+ # file_type (str): "csv" or "json".
84
+ # db (Session): The database session object.
85
+
86
+ # Returns:
87
+ # int: The number of users successfully uploaded.
88
+ # """
89
+ # uploaded_count = 0
90
+
91
+ # try:
92
+ # if file_type == "csv":
93
+ # data = read_csv(io.BytesIO(contents), converters={"roles": lambda x: x.split(",")})
94
+ # #
95
+ # # records = data.to_dict(orient='records')
96
+ # elif file_type == "json":
97
+ # data = read_json(io.BytesIO(contents))
98
+ # # records = data.to_dict(orient='records')
99
+ # else:
100
+ # raise ValueError("Invalid file type. Supported types: csv, json")
101
+ # data["password"] = data["password"].apply(encrypt_pass)
102
+ # data["status"] = StatusEnum.NEW
103
+ # records = data.to_dict(orient='records')
104
+ # user_dtos = [AppuserDto(**row) for row in records]
105
+
106
+ # db.bulk_insert_mappings(Appuser, [user_dto.__dict__ for user_dto in user_dtos])
107
+ # db.commit()
108
+ # uploaded_count = len(records)
109
+
110
+ # except ValueError as ve:
111
+ # raise ve
112
+ # except (csv.Error, json.JSONDecodeError) as file_error:
113
+ # raise Exception(f"Error reading the file: {file_error}")
114
+ # except Exception as e:
115
+ # db.rollback()
116
+ # raise Exception(f"Error processing user data: {e}")
117
+
118
+ # return uploaded_count
119
+
120
+ def get_user_by_id(usr: int, db: Session) -> UserResponse:
121
+ try:
122
+ user = pg.get_model(Appuser, db, usr)
123
+ if not user:
124
+ raise ValueError(f"No user with id {usr}")
125
+ return UserResponse(**user.__dict__)
126
+ except Exception as e:
127
+ logger.error(f"Get user by id failed {str(e)}")
128
+ raise ValueError(f"Error getting User {usr}")
129
+ def activate_user(email: str, db: Session) -> bool:
130
+ try:
131
+ user = db.query(Appuser).filter(Appuser.email == email).first()
132
+ if not user:
133
+ raise ValueError(f"No user with email {email}")
134
+ user.is_active = not user.is_active
135
+ db.commit()
136
+ return True
137
+ except Exception as e:
138
+ db.rollback()
139
+ logger.error(f"Error activating user {email}: {str(e)}")
140
+ raise ValueError(f"Error activating user {email}: {str(e)}")
141
+ def update_user(user_update: AppuserUpdate, db: Session) -> UserResponse:
142
+ try:
143
+ existing_user = db.query(Appuser).get(user_update.id)
144
+ if not existing_user:
145
+ raise ValueError(f"User not found. {user_update.id}")
146
+
147
+ # Update the address if provided
148
+ if user_update.address:
149
+ if existing_user.address_id:
150
+ addr = db.query(Address).get(existing_user.address_id)
151
+ for key, value in user_update.address.__dict__.items():
152
+ setattr(addr, key, value)
153
+ addr.id = existing_user.address_id
154
+ db.commit()
155
+ db.refresh(addr)
156
+ else:
157
+ addr = Address(**user_update.address.__dict__)
158
+ db.add(addr)
159
+ db.commit()
160
+ db.refresh(addr)
161
+ existing_user.address_id = addr.id
162
+
163
+ # Update the user details
164
+ for key, value in user_update.__dict__.items():
165
+ if key != 'address' and key != 'id':
166
+ setattr(existing_user, key, value)
167
+ db.commit()
168
+ db.refresh(existing_user)
169
+
170
+ logger.info(f"User {existing_user.email} updated successfully")
171
+ return UserResponse(**existing_user.__dict__)
172
+
173
+ except IntegrityError as e:
174
+ db.rollback()
175
+ if "duplicate key value violates unique constraint" in str(e.orig):
176
+ logger.error(f"Error updating user: {e}")
177
+ raise ValueError(f"Email already exists: {user_update.email}")
178
+ else:
179
+ logger.error(f"Integrity error updating user: {e}")
180
+ raise ValueError("An error occurred during data integrity validation.")
181
+ except SQLAlchemyError as e:
182
+ db.rollback()
183
+ logger.error(f"Database error updating user: {e}")
184
+ raise ValueError("Database error.")
185
+ except Exception as e:
186
+ logger.error(f"Unexpected error: {e}")
187
+ raise ValueError(f"Unexpected error: {e}")
188
+
189
+ def update_photo(user_id: int, photo: str, db: Session) -> bool:
190
+ try:
191
+ user = db.query(Appuser).get(user_id)
192
+ user.dp = photo
193
+ db.commit()
194
+ return True
195
+ except Exception as e:
196
+ raise e
197
+
198
+ def get_userby_username(email: str,db:Session) -> Appuser:
199
+ user = db.query(Appuser).filter(Appuser.email == email).first()
200
+ return user
201
+
202
+
203
+ def all_users(db: Session) -> list[UserResponse]:
204
+ try:
205
+ users = pg.get_all_models(Appuser, db)
206
+ users_resp = [
207
+ UserResponse(
208
+ **user.__dict__
209
+ )
210
+ for user in users
211
+ ]
212
+ return users_resp
213
+ except SQLAlchemyError as e:
214
+ logger.error(f"Database error retrieving all users: {e}")
215
+ raise ValueError("Database error.")
216
+ except Exception as e:
217
+ logger.error(f"Unexpected error retrieving all users: {e}")
218
+ raise ValueError("Unexpected error.")
219
+ def delete_user(db: Session, id: int) -> bool:
220
+ try:
221
+ user = db.query(Appuser).get(id)
222
+ if user is None:
223
+ raise ValueError(f"User with id {id} not found.")
224
+ db.delete(user)
225
+ db.commit()
226
+ return True
227
+ except Exception as e:
228
+ raise ValueError(f"An error occurred. {e}")
229
+ def set_delete_user(db: Session, id: int) -> bool:
230
+ try:
231
+ user = db.query(Appuser).get(id)
232
+ if user is None:
233
+ raise ValueError(f"User with id {id} not found.")
234
+
235
+ user.status = StatusEnum.DELETED
236
+ db.commit()
237
+ return True
238
+
239
+ except Exception as e:
240
+ db.rollback() # Rollback in case of an error
241
+ logger.error(f"Error deleting user with id {id}: {e}")
242
+ raise ValueError(f"An error occurred deleting user with id {id}.")
243
+
244
+
245
+ def users_by_role(db: Session, role: AccessRoleEnum) -> list[UserResponse]:
246
+ """
247
+ Retrieve users based on their role.
248
+
249
+ Args:
250
+ db (Session): Database session.
251
+ role (str): Role to filter users.
252
+
253
+ Returns:
254
+ List[UserResponse]: List of users matching the specified role.
255
+ """
256
+ users = db.query(Appuser).filter(Appuser.roles.contains([role])).order_by(desc(Appuser.id)).all()
257
+ users_resp = [
258
+ UserResponse(
259
+ id=user.id,
260
+ timestamp=user.timestamp,
261
+ first_name=user.first_name,
262
+ last_name=user.last_name,
263
+ email=user.email,
264
+ password=user.password,
265
+ is_active=user.is_active,
266
+ roles=user.roles,
267
+ gender=user.gender,
268
+ socialmedia=user.socialmedia
269
+ )
270
+ for user in users
271
+ ]
272
+ return users_resp
273
+
274
+
275
+ def is_exist(email: EmailStr, db: Session) -> int:
276
+ user = db.query(Appuser).filter(Appuser.email == email).first()
277
+ return user.id if user else 0
278
+
279
+ async def rest_password_token(email: str, db: Session) -> Appuser | None:
280
+ try:
281
+ user = db.query(Appuser).filter(Appuser.email == email).first()
282
+ if not user:
283
+ raise ValueError(f"No user with email {email}")
284
+ token = str(uuid.uuid4())[:8]
285
+ user.token = token
286
+ db.commit()
287
+ # mailer=MailerDto(
288
+ # recipients=[email],
289
+ # subject="Password Reset",
290
+ # message=f"Your password reset token is {token}",
291
+ # create_at=datetime.now(),
292
+ # is_html=False
293
+ # )
294
+ # asyncio.create_task(send_email_sendgrid(mailer))
295
+ # logger.info("Password reset token %s sent to %s", token, email)
296
+ return user
297
+ except Exception as e:
298
+ db.rollback()
299
+ logger.error(f"Error resetting password for {email}: {str(e)}")
300
+ def reset_password(reset: UserReset, db: Session) -> bool:
301
+ try:
302
+ email = reset.email
303
+ user = db.query(Appuser).filter(
304
+ (Appuser.email == email) & (Appuser.token == reset.token)
305
+ ).first()
306
+ logger.info("compare %s and ",user.token, reset.token)
307
+ if not user:
308
+ raise ValueError(f"No user with email {email} or token {reset.token}")
309
+ user.password = encrypt_pass(reset.password)
310
+ db.commit()
311
+ return True
312
+ except Exception as e:
313
+ db.rollback()
314
+ logger.error(f"Error resetting password for: {str(e)}")
315
+ return False
316
+ def manual_change_password(user: UserReset, db: Session) -> bool:
317
+ try:
318
+ exist_user = db.query(Appuser).filter(Appuser.email == user.email).first()
319
+ if not exist_user:
320
+ raise ValueError(f"No user with email {user.email}")
321
+ if not verify_password(user.password, exist_user.password):
322
+ raise ValueError("Incorrect username or password") from None
323
+ if user.new_password is None:
324
+ raise ValueError("New password cannot be None")
325
+ exist_user.password = encrypt_pass(user.new_password)
326
+ db.commit()
327
+ return True
328
+ except Exception as e:
329
+ db.rollback()
330
+ logger.error(f"Error resetting password for {user.email}: {str(e)}")
331
+ return False
332
+ def create_address(address: AddressDto, db: Session) -> bool:
333
+ address = pg.add_model(Address,db, **address.model_dump())
334
+ 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.1.0"
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"