sharedkernel 2.1.2__py3-none-any.whl → 2.2.1__py3-none-any.whl

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.
@@ -1 +1,3 @@
1
- # from .mongo_repository_base import MongoRepositoryBase
1
+ from .mongo_generic_repository import MongoGenericRepository
2
+ from .mongo_generic_audit_repository import MongoGenericAuditRepository
3
+ from .audit_model import AuditLog, AuditOperation
@@ -0,0 +1,17 @@
1
+ from sharedkernel.objects.base_document import BaseDocument
2
+
3
+ from enum import Enum
4
+
5
+ class AuditOperation(str, Enum):
6
+ CREATE = "create"
7
+ UPDATE = "update"
8
+ DELETE = "delete"
9
+
10
+ class AuditLog(BaseDocument):
11
+ user_id: str
12
+ collection_name: str
13
+ document_id: str
14
+ operation: AuditOperation
15
+ original: dict | None = None
16
+ modified: dict
17
+
@@ -0,0 +1,72 @@
1
+ from typing import Generic, TypeVar, List, Type
2
+ from bson import ObjectId
3
+ from pymongo import MongoClient
4
+ from pydantic.v1 import BaseModel
5
+
6
+ from sharedkernel.database.audit_model import AuditLog
7
+
8
+ T = TypeVar("T", bound=BaseModel)
9
+
10
+ AUDIT_COLLECTION_NAME = "audit_log"
11
+
12
+ class MongoGenericAuditRepository():
13
+ def __init__(
14
+ self,
15
+ database: MongoClient,
16
+ audit_collection_name: str | None = None
17
+ ):
18
+ self.database = database
19
+ self.__collection_name = audit_collection_name or AUDIT_COLLECTION_NAME
20
+
21
+ self.collection = self.database[self.__collection_name]
22
+
23
+ def _map_to_model(self, document: dict) -> AuditLog:
24
+ document["id"] = str(document.pop("_id"))
25
+ return AuditLog(**document)
26
+
27
+ def find_one(self, id: str) -> T:
28
+ query = {"_id": ObjectId(id), "is_deleted": False}
29
+ result = self.collection.find_one(query)
30
+ return self._map_to_model(result) if result else None
31
+
32
+ def find(
33
+ self,
34
+ document_id: str | None = None,
35
+ operation: str | None = None,
36
+ collection_name: str | None = None,
37
+ field_name: str | None = None,
38
+ ) -> List[AuditLog]:
39
+
40
+ query = {}
41
+
42
+ if document_id:
43
+ query["document_id"] = document_id
44
+ if operation:
45
+ query["operation"] = operation
46
+ if collection_name:
47
+ query["collection_name"] = collection_name
48
+
49
+ if field_name:
50
+ query["$or"] = [
51
+ {f"original.{field_name}": {"$exists": True}},
52
+ {f"modified.{field_name}": {"$exists": True}},
53
+ ]
54
+
55
+ results = self.collection.find(query)
56
+ return [self._map_to_model(bot) for bot in results]
57
+
58
+ def insert_one(self, data: AuditLog) -> str:
59
+ delattr(data, "id")
60
+ result = self.collection.insert_one(data.model_dump())
61
+ return str(result.inserted_id)
62
+
63
+ def insert_many(self, data: List[AuditLog]) -> List[str]:
64
+ data_list = [delattr(d.model_dump(), "id") for d in data]
65
+ result = self.collection.insert_many(data_list)
66
+ return [str(id_) for id_ in result.inserted_ids]
67
+
68
+ def delete_one(self, id: str) -> int:
69
+ query = {"_id": ObjectId(id)}
70
+ result = self.collection.delete_one(query)
71
+ return result.deleted_count
72
+
@@ -1,18 +1,31 @@
1
- from pymongo import MongoClient
2
1
  from bson import ObjectId
3
2
  from typing import Generic, TypeVar, List, Type
4
3
  from pydantic.v1 import BaseModel
4
+ from pymongo import MongoClient, ReturnDocument
5
+ from contextlib import suppress
6
+
7
+ from sharedkernel.objects.user_info import current_user_info
5
8
  from sharedkernel.string_extentions import camel_to_snake
9
+ from sharedkernel.objects.base_document import BaseAuditDocument
10
+ from sharedkernel.database.mongo_generic_audit_repository import MongoGenericAuditRepository
11
+ from sharedkernel.database.audit_model import (
12
+ AuditLog,
13
+ AuditOperation
14
+ )
15
+ from sharedkernel.diff_utils import generate_clean_diff
6
16
 
7
17
  T = TypeVar("T", bound=BaseModel)
8
18
 
9
-
10
19
  class MongoGenericRepository(Generic[T]):
11
- def __init__(self, database: MongoClient, model: Type[T]):
20
+ def __init__(self, database: MongoClient, model: Type[T], audit_collection_name: str | None = None):
12
21
  self.database = database
13
22
  self.__collection_name = camel_to_snake(model.__name__)
14
23
  self.collection = self.database[self.__collection_name]
15
24
  self.model = model
25
+ self.audit_collection = MongoGenericAuditRepository(
26
+ database=database,
27
+ audit_collection_name=audit_collection_name
28
+ )
16
29
 
17
30
  def _map_to_model(self, document: dict) -> T:
18
31
  document["id"] = str(document.pop("_id"))
@@ -25,19 +38,77 @@ class MongoGenericRepository(Generic[T]):
25
38
 
26
39
  def insert_one(self, data: T) -> str:
27
40
  delattr(data, "id")
28
- result = self.collection.insert_one(data.dict())
41
+ result = self.collection.insert_one(data.model_dump())
42
+
43
+ # For Audit log
44
+ with suppress(Exception):
45
+ if isinstance(data, BaseAuditDocument):
46
+ document = AuditLog(
47
+ user_id=current_user_info.get().nameid,
48
+ collection_name=self.__collection_name,
49
+ document_id=str(result.inserted_id),
50
+ operation=AuditOperation.CREATE,
51
+ original=None,
52
+ modified=data.model_dump(),
53
+ )
54
+ self.audit_collection.insert_one(document)
55
+
29
56
  return str(result.inserted_id)
30
57
 
31
58
  def insert_many(self, data: List[T]) -> List[str]:
32
- data_list = [delattr(d.dict(), "id") for d in data]
59
+ data_list = [delattr(d.model_dump(), "id") for d in data]
33
60
  result = self.collection.insert_many(data_list)
61
+
62
+ # For Audit log
63
+ with suppress(Exception):
64
+ if len(result.inserted_ids) > 0:
65
+ if isinstance(data[0], BaseAuditDocument):
66
+ documents = []
67
+ for i, d in enumerate(data):
68
+ document = AuditLog(
69
+ user_id=current_user_info.get().nameid,
70
+ collection_name=self.__collection_name,
71
+ document_id=str(result.inserted_ids[i]),
72
+ operation=AuditOperation.CREATE,
73
+ original=None,
74
+ modified=d.model_dump(),
75
+ )
76
+ documents.append(document)
77
+
78
+ self.audit_collection.insert_many(documents)
79
+
34
80
  return [str(id_) for id_ in result.inserted_ids]
35
81
 
36
82
  def update_one(self, id: str, data: T) -> int:
37
83
  delattr(data, "id")
84
+
38
85
  query = {"_id": ObjectId(id)}
39
- result = self.collection.update_one(query, {"$set": data.dict()})
40
- return result.modified_count
86
+ before_dict = self.collection.find_one_and_update(
87
+ query,
88
+ {"$set": data.model_dump()},
89
+ return_document=ReturnDocument.BEFORE
90
+ )
91
+
92
+ before_model = self.model(**before_dict)
93
+
94
+ # For Audit log
95
+ with suppress(Exception):
96
+ diff_data = generate_clean_diff(before_model, data)
97
+ old_data, new_data = diff_data["original"], diff_data["modified"]
98
+
99
+ if isinstance(data, BaseAuditDocument) and new_data:
100
+ document = AuditLog(
101
+ user_id=current_user_info.get().nameid,
102
+ collection_name=self.__collection_name,
103
+ document_id=id,
104
+ operation=AuditOperation.UPDATE,
105
+ original=old_data,
106
+ modified=new_data,
107
+ )
108
+
109
+ self.audit_collection.insert_one(document)
110
+
111
+ return 1
41
112
 
42
113
  def delete_one(self, id: str) -> int:
43
114
  query = {"_id": ObjectId(id)}
@@ -0,0 +1,65 @@
1
+ from deepdiff import DeepDiff
2
+ from pydantic.v1 import BaseModel
3
+
4
+ EXCLUDE_FIELDS = {"created_on", "updated_on"}
5
+
6
+ def generate_clean_diff(before: BaseModel, after: BaseModel, exclude_fields: set = EXCLUDE_FIELDS):
7
+ before_dict = before.model_dump()
8
+ after_dict = after.model_dump()
9
+ try:
10
+ clean_before = {k: v for k, v in before_dict.items() if k not in exclude_fields}
11
+ clean_after = {k: v for k, v in after_dict.items() if k not in exclude_fields}
12
+
13
+ diff = DeepDiff(clean_before, clean_after, ignore_order=True)
14
+ original = {}
15
+ modified = {}
16
+
17
+ for change_type in ["values_changed", "type_changes"]:
18
+ changes = diff.get(change_type, {})
19
+ for path, change in changes.items():
20
+ keys = path.strip("root").strip("[").strip("]").replace("][", ".").replace("'", "").split(".")
21
+ _apply_nested_keys(original, keys, change["old_value"])
22
+ _apply_nested_keys(modified, keys, change["new_value"])
23
+
24
+ return {"original": original, "modified": modified}
25
+
26
+ except Exception as e:
27
+ return {"original": {}, "modified": {}, "error": str(e)}
28
+
29
+ def _apply_nested_keys(base: dict, keys: list, value):
30
+ current = base
31
+ for i, key in enumerate(keys):
32
+ is_last = i == len(keys) - 1
33
+ next_key = keys[i + 1] if not is_last else None
34
+
35
+ # Determine if current key is an index (for lists)
36
+ is_index = key.isdigit()
37
+ key = int(key) if is_index else key
38
+
39
+ # Ensure current is list if key is int
40
+ if is_index and not isinstance(current, list):
41
+ # Convert current to list if it isn't already
42
+ raise TypeError(f"Expected list at {keys[:i]}, got {type(current).__name__}")
43
+
44
+ # Handle last key
45
+ if is_last:
46
+ if isinstance(current, list):
47
+ # Ensure list is big enough
48
+ while len(current) <= key:
49
+ current.append(None)
50
+ current[key] = value
51
+ else:
52
+ current[key] = value
53
+ return
54
+
55
+ # Prepare next container (dict or list)
56
+ if isinstance(current, list):
57
+ while len(current) <= key:
58
+ current.append(None)
59
+ if current[key] is None:
60
+ current[key] = {} if (next_key and not next_key.isdigit()) else []
61
+ current = current[key]
62
+ else:
63
+ if key not in current:
64
+ current[key] = {} if (next_key and not next_key.isdigit()) else []
65
+ current = current[key]
@@ -2,10 +2,9 @@ import jwt
2
2
  import time
3
3
  from fastapi import Request
4
4
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
5
-
6
5
  from sharedkernel.exception.exception import UnAuthorizedException
7
6
  from sharedkernel.objects import JwtModel
8
-
7
+ from sharedkernel.objects.user_info import current_user_info, UserInfo
9
8
 
10
9
  class JWTBearer(HTTPBearer):
11
10
 
@@ -18,21 +17,28 @@ class JWTBearer(HTTPBearer):
18
17
  credentials: HTTPAuthorizationCredentials = await super(
19
18
  JWTBearer, self
20
19
  ).__call__(request)
21
-
22
20
  decoded_token = self.verify(credentials.credentials)
23
21
  request.state.decoded_token = decoded_token
24
22
 
23
+ nameid = decoded_token.get("nameid") or decoded_token.get("id")
24
+
25
+ user_info = UserInfo(
26
+ nameid=nameid, email=decoded_token.get("email"), unique_name=decoded_token.get("unique_name")
27
+ )
28
+
29
+ current_user_info.set(user_info)
30
+
25
31
  except:
26
32
  raise UnAuthorizedException()
27
33
 
28
34
 
29
35
  def verify(self, token: str) -> dict:
30
36
  decoded_token = jwt.decode(
31
- jwt= token.replace("Bearer","").strip(),
32
- key= self.jwt_config.secret_key,
33
- algorithms= self.jwt_config.algorithms,
34
- audience= self.jwt_config.audience,
35
- issuer= self.jwt_config.issuer
37
+ jwt= token.replace("Bearer","").strip(),
38
+ key= self.jwt_config.secret_key,
39
+ algorithms= self.jwt_config.algorithms,
40
+ audience= self.jwt_config.audience,
41
+ issuer= self.jwt_config.issuer
36
42
  )
37
43
 
38
44
  if decoded_token["exp"] < time.time():
@@ -1,4 +1,5 @@
1
- from .base_document import BaseDocument
1
+ from .base_document import BaseDocument, BaseAuditDocument
2
2
  from .jwt_model import JwtModel
3
3
  from .result import Result
4
- from .json_string_model import JsonStringModel
4
+ from .json_string_model import JsonStringModel
5
+ from .user_info import UserInfo
@@ -6,4 +6,8 @@ class BaseDocument(BaseModel):
6
6
  id: str | None = None
7
7
  is_deleted: bool | None = False
8
8
  created_on: datetime = Field(default_factory=datetime.now)
9
- updated_on: datetime = Field(default_factory=datetime.now)
9
+ updated_on: datetime = Field(default_factory=datetime.now)
10
+
11
+
12
+ class BaseAuditDocument(BaseDocument):
13
+ pass
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+ from contextvars import ContextVar
4
+
5
+ class UserInfo(BaseModel):
6
+ nameid: str
7
+ unique_name: str | None = None
8
+ email: str | None = None
9
+ role: str | None = None
10
+
11
+
12
+ current_user_info: ContextVar[UserInfo] = ContextVar("current_user_info")
13
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sharedkernel
3
- Version: 2.1.2
3
+ Version: 2.2.1
4
4
  Summary: sharekernel is a shared package between all python projects
5
5
  Author: Smilinno
6
6
  Description-Content-Type: text/markdown
@@ -18,6 +18,7 @@ Requires-Dist: python-docx
18
18
  Requires-Dist: mammoth
19
19
  Requires-Dist: markdown
20
20
  Requires-Dist: beautifulsoup4
21
+ Requires-Dist: deepdiff
21
22
  Dynamic: author
22
23
  Dynamic: description
23
24
  Dynamic: description-content-type
@@ -28,6 +29,10 @@ Dynamic: summary
28
29
  this is a shared kernel package
29
30
 
30
31
  # Change Log
32
+ ### Version 2.2.1
33
+ - fixbug requirements
34
+ ### Version 2.2.0
35
+ - implement Audit logging
31
36
  ### Version 2.1.2
32
37
  - Update pydantic parse_object with model_validate
33
38
  ### Version 2.1.0
@@ -1,12 +1,15 @@
1
1
  sharedkernel/common.py,sha256=HL5vsuJBaIeBcoXA8Hbe6jnYAv4danIszo5Y7G2gGDA,622
2
2
  sharedkernel/data_format_converter.py,sha256=GWGbfhKJBifkz-cfnqKAFjJM43WC0qdq9KSELj3xR30,3774
3
3
  sharedkernel/date_converter.py,sha256=Cjd4ewm0pIfQzv7nlgAAB_EYrr-VvXxQGehJCNphgXc,4491
4
- sharedkernel/jwt_service.py,sha256=Gmi09LtUp4ghjHluJts7LM4sQzqt7mNl8khDfC0FzwU,1296
4
+ sharedkernel/diff_utils.py,sha256=mtwJmc05GAXUOB0ZLtqAhfBT1kGoSQ7qmP5N44P73ho,2564
5
+ sharedkernel/jwt_service.py,sha256=uEyOEWS5Y7eFIyGMu9fjRWdLEOcJtG-iL8Lk3QB5QYs,1666
5
6
  sharedkernel/regex_masking.py,sha256=zQrgteP8Cuq1EC9B7QUJqAXUxK9ISD9kWMYK2AbRfw0,3288
6
7
  sharedkernel/s3_uploader.py,sha256=9Tzi5siceHPKab244tOAmnY_hNOeTn9yvsQevNdz7Gk,3313
7
8
  sharedkernel/string_extentions.py,sha256=ld02W06gd0Ql80GQU6nlqPAeUSfOe2Yr8cCzf3lJgQY,98
8
- sharedkernel/database/__init__.py,sha256=-g-UAb-oHn4YmHys7Bgu3MtvgTXeRp_PGsih_foGJOU,56
9
- sharedkernel/database/mongo_generic_repository.py,sha256=eESToVTTPFa1XqFTgRaqUDgSXCD1w1mTabubp_2E6hM,1976
9
+ sharedkernel/database/__init__.py,sha256=G_wHQf2WCrfyuPXHOOQFkcd0BkJQLmD8iginIyZWfjk,182
10
+ sharedkernel/database/audit_model.py,sha256=SMAYrvMb7XvPi4076TDgkOLaxh_3Jg_tfE12qKOy8RA,364
11
+ sharedkernel/database/mongo_generic_audit_repository.py,sha256=tJGp1hHhyzcYOV9af36QzLXeT_appNNlSs0lxqlofzk,2327
12
+ sharedkernel/database/mongo_generic_repository.py,sha256=BkmJcJ3ncUpO0vahNvaD3al_AB0kZK2FOYPp1BPaHp0,4781
10
13
  sharedkernel/enum/__init__.py,sha256=9M3H7paIFx-0GRhpod_Jg30nxIYPjskGL9WF9rN9U5I,33
11
14
  sharedkernel/enum/error_code.py,sha256=qF_YJrHHDc6ibAClSxBMJ9ktzG2OtvGxb67LX0O-NG0,475
12
15
  sharedkernel/exception/__init__.py,sha256=Yjkd1VsLFuzfWcMj-v8YYTA-vjtUlF4ESglBFeN1zvs,315
@@ -16,12 +19,13 @@ sharedkernel/normalizer/__init__.py,sha256=cDmbquAW6o7rnvv3XkI7h5vMYp-3NmGN6zp1r
16
19
  sharedkernel/normalizer/number_normalizer.py,sha256=zJ700T0t9P7hgxp7vox98LdPw9A4jsUjIHA_II9YmqU,286
17
20
  sharedkernel/normalizer/phone_number_normalizer.py,sha256=8z-JGWvH45GiOkceMz9jZ4gLoW9X120I1JSzCAGWcvU,19268
18
21
  sharedkernel/normalizer/string_normalizer.py,sha256=ev69dwM6SiMQwDPSzQWm8jFb6hTRFnop3K01YcTlmrA,841
19
- sharedkernel/objects/__init__.py,sha256=XIbTfxi8W9LQgouGAA1kv_o1TV3Ry6oX0u7WutE0Bsg,145
20
- sharedkernel/objects/base_document.py,sha256=1h-t-3-losXcBH_LnsZ7eULBgSkZmJqeR_iO7KY9np4,288
22
+ sharedkernel/objects/__init__.py,sha256=-vhYnxZ8Dw9U7NMkxwiGUUPS-MTC6u1t6WJAOS-w0eE,196
23
+ sharedkernel/objects/base_document.py,sha256=TERbRaZaW_zExm7Ddpk_3nCwMvSroGC3KkdTeWsqzNQ,339
21
24
  sharedkernel/objects/json_string_model.py,sha256=j63tnoqiok0EmBP6T-ChYuQYKPw7mLqUOHuaCDUZkmw,1333
22
25
  sharedkernel/objects/jwt_model.py,sha256=XQHQhTbg7PT8XiUh5fd9MwRH4ldPsesI_hfbjaSqdKg,134
23
26
  sharedkernel/objects/result.py,sha256=I_9hX5TPEO1oStzuFLjFh1rtimXorz7ml-OaW_2BMvc,680
24
- sharedkernel-2.1.2.dist-info/METADATA,sha256=RVskHdhtMeuqto3gI2518-jJ-kGyQR8mWeRsHZDWllQ,2815
25
- sharedkernel-2.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- sharedkernel-2.1.2.dist-info/top_level.txt,sha256=TVTOnV1MItSSlpSjqkiijuHkoVsGHS4CArpsM-lylkE,13
27
- sharedkernel-2.1.2.dist-info/RECORD,,
27
+ sharedkernel/objects/user_info.py,sha256=RsOExvd--7CvBibOA-7ZLLmoW3_rh9Hb3tTkH5K3DN8,283
28
+ sharedkernel-2.2.1.dist-info/METADATA,sha256=ePL86RY8VoPcXTcGfp04AF2ojqB0uzLuDdnWH7tzk8Q,2923
29
+ sharedkernel-2.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ sharedkernel-2.2.1.dist-info/top_level.txt,sha256=TVTOnV1MItSSlpSjqkiijuHkoVsGHS4CArpsM-lylkE,13
31
+ sharedkernel-2.2.1.dist-info/RECORD,,