sharedkernel 2.7.0__py3-none-any.whl → 2.9.0__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.
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+ import re
3
+ from typing import List, Optional, Protocol, Tuple
4
+
5
+
6
+ class SplitRule(Protocol):
7
+ """Interface for chunk split rules."""
8
+
9
+ def apply(
10
+ self, segment: str, offset: int
11
+ ) -> Tuple[Optional[int], Optional[str], Optional[str]]: ...
12
+
13
+
14
+ class PunctuationRule(SplitRule):
15
+ """Split after the first matching punctuation character."""
16
+
17
+ def __init__(self, punctuations: str = ".,;:!?"):
18
+ self.punctuations = set(punctuations)
19
+
20
+ def apply(
21
+ self, segment: str, offset: int
22
+ ) -> Tuple[Optional[int], Optional[str], Optional[str]]:
23
+ for i, ch in enumerate(segment):
24
+ if ch in self.punctuations:
25
+ return offset + i + 1, ch, "punctuation"
26
+ return None, None, None
27
+
28
+
29
+ class RegexRule(SplitRule):
30
+ """Split after the first regex match."""
31
+
32
+ def __init__(self, pattern: str):
33
+ self.regex = re.compile(pattern) if pattern else None
34
+
35
+ def apply(
36
+ self, segment: str, offset: int
37
+ ) -> Tuple[Optional[int], Optional[str], Optional[str]]:
38
+ if not self.regex:
39
+ return None, None, None
40
+ m = self.regex.search(segment)
41
+ if m:
42
+ return offset + m.end(), segment[m.start() : m.end()], "regex"
43
+ return None, None, None
44
+
45
+
46
+ class CutWordRule(SplitRule):
47
+ """Split after the first occurrence of any configured word/phrase."""
48
+
49
+ def __init__(self, cut_words: List[str]):
50
+ self.cut_words = [w.lower() for w in cut_words]
51
+
52
+ def apply(
53
+ self, segment: str, offset: int
54
+ ) -> Tuple[Optional[int], Optional[str], Optional[str]]:
55
+ lowered = segment.lower()
56
+ for word in self.cut_words:
57
+ idx = lowered.find(word)
58
+ if idx != -1:
59
+ phrase = segment[idx : idx + len(word)]
60
+ return offset + idx + len(word), phrase, "cut_word"
61
+ return None, None, None
62
+
63
+
64
+ class SpaceFallbackRule(SplitRule):
65
+ """Split near the center on space; otherwise hard-cut at max_len."""
66
+
67
+ def __init__(self, max_len: int):
68
+ self.max_len = max_len
69
+
70
+ def apply(
71
+ self, segment: str, offset: int
72
+ ) -> Tuple[Optional[int], Optional[str], Optional[str]]:
73
+ centre = len(segment) // 2
74
+ left = segment.rfind(" ", 0, centre)
75
+ right = segment.find(" ", centre)
76
+
77
+ if left != -1:
78
+ split = left
79
+ elif right != -1:
80
+ split = right
81
+ else:
82
+ split = self.max_len
83
+
84
+ return offset + split, None, "space_fallback"
@@ -0,0 +1,107 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Optional
3
+
4
+ from .chunk_rule import (
5
+ SplitRule,
6
+ )
7
+
8
+
9
+ @dataclass
10
+ class Chunk:
11
+ """A single chunk produced by TextChunker."""
12
+
13
+ text: str
14
+ number: int
15
+ begin: int
16
+ end: int
17
+ method: str
18
+ phrase: Optional[str]
19
+
20
+
21
+ class TextChunker:
22
+ """Split text into chunks based on ordered split rules."""
23
+
24
+ def __init__(self, min_len: int, max_len: int, rules: List[SplitRule]):
25
+ if min_len < 1:
26
+ raise ValueError("min_len must be >= 1")
27
+ if max_len < min_len:
28
+ raise ValueError("max_len must be >= min_len")
29
+
30
+ self.min_len = min_len
31
+ self.max_len = max_len
32
+ self.rules = rules
33
+
34
+ def chunk(self, text: str) -> List[Chunk]:
35
+ if not text:
36
+ return []
37
+
38
+ chunks: List[Chunk] = []
39
+ pos = 0
40
+ number = 1
41
+
42
+ while pos < len(text):
43
+ if len(text) - pos <= self.max_len:
44
+ chunk_text = text[pos:]
45
+ chunks.append(Chunk(chunk_text, number, pos, len(text), "final", None))
46
+ break
47
+
48
+ window_end = pos + self.max_len
49
+ window = text[pos:window_end]
50
+
51
+ split_idx: Optional[int] = None
52
+ phrase: Optional[str] = None
53
+ method: Optional[str] = None
54
+
55
+ for rule in self.rules:
56
+ candidate, cand_phrase, cand_method = rule.apply(window, pos)
57
+ if candidate is not None and candidate - pos >= self.min_len:
58
+ split_idx, phrase, method = candidate, cand_phrase, cand_method
59
+ break
60
+
61
+ if split_idx is None:
62
+ split_idx = pos + self.max_len
63
+ method = "hard_cut"
64
+
65
+ chunk_text = text[pos:split_idx]
66
+ chunks.append(Chunk(chunk_text, number, pos, split_idx, method, phrase))
67
+ pos = split_idx
68
+ number += 1
69
+
70
+ return chunks
71
+
72
+ def chunks(self, text: str):
73
+ """Yield chunks lazily, using the same logic as `chunk`."""
74
+ if not text:
75
+ return
76
+
77
+ pos = 0
78
+ number = 1
79
+
80
+ while pos < len(text):
81
+ if len(text) - pos <= self.max_len:
82
+ chunk_text = text[pos:]
83
+ yield Chunk(chunk_text, number, pos, len(text), "final", None)
84
+ break
85
+
86
+ window_end = pos + self.max_len
87
+ window = text[pos:window_end]
88
+
89
+ split_idx: Optional[int] = None
90
+ phrase: Optional[str] = None
91
+ method: Optional[str] = None
92
+
93
+ for rule in self.rules:
94
+ candidate, cand_phrase, cand_method = rule.apply(window, pos)
95
+ if candidate is not None and candidate - pos >= self.min_len:
96
+ split_idx, phrase, method = candidate, cand_phrase, cand_method
97
+ break
98
+
99
+ if split_idx is None:
100
+ split_idx = pos + self.max_len
101
+ method = "hard_cut"
102
+
103
+ chunk_text = text[pos:split_idx]
104
+ yield Chunk(chunk_text, number, pos, split_idx, method, phrase)
105
+
106
+ pos = split_idx
107
+ number += 1
sharedkernel/config.py CHANGED
@@ -26,7 +26,6 @@ IP_SESSION_CACHE_DATABASE_NUMBER = os.getenv("IP_SESSION_CACHE_DATABASE_NUMBER")
26
26
  REDIS_URL = os.getenv("REDIS_URL")
27
27
  REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
28
28
  REDIS_USER = os.getenv("REDIS_USER")
29
- REDIS_CONNECTION_STRING = os.getenv("REDIS_CONNECTION_STRING")
30
29
  CACHE_DATABASE_NUMBER = os.getenv("CACHE_DATABASE_NUMBER")
31
30
  REDIS_CONNECTION_STRING = os.getenv(
32
31
  "REDIS_CONNECTION_STRING", f"redis://{REDIS_USER}:{REDIS_PASSWORD}@{REDIS_URL}"
@@ -2,7 +2,7 @@ from typing import Generic, Optional, Type, TypeVar
2
2
  from pydantic import BaseModel
3
3
  from redis.asyncio import Redis
4
4
 
5
- from cache.cache_repository import CacheRepository
5
+ from .cache_repository import CacheRepository
6
6
 
7
7
  T = TypeVar("T", bound=BaseModel)
8
8
 
@@ -1,2 +1,7 @@
1
1
  from .error_code import ErrorCode
2
- from .redis_mode_enum import RedisMode
2
+ from .redis_mode_enum import RedisMode
3
+ from .feature_key import FeatureKey
4
+ from .plan_type import PlanType
5
+ from .plan_interval import PlanInterval
6
+ from .order_type import OrderType
7
+ from .feature_display_type import FeatureDisplayType
@@ -9,3 +9,13 @@ class ErrorCode(str, Enum):
9
9
  Intents_Count_Should_Equal_One = "فقط یک اینتنت میتوانید وارد نمایید"
10
10
  Unsupported_Date_Type = "تاریخ داده شده پیشتیبانی نمیشود."
11
11
  Unsupported_IP = "آی پی داده شده معتبر نمی باشد"
12
+ Subscription_Not_Found = "اشتراک فعالی برای شما یافت نشد"
13
+ Subscription_Insufficient_Bot_Feature = "اعتبار شما برای ساخت بات کافی نیست"
14
+ Subscription_Insufficient_Message_Feature = "اعتبار شما برای ارسال پیام کافی نیست"
15
+ Subscription_Insufficient_URL_FEATURE = "اعتبار تعداد پردازش آدرس شما به اتمام رسیده"
16
+ Subscription_Insufficient_TEXT_FEATURE = "اعتبار تعداد پردازش متن شما به اتمام رسیده"
17
+ Subscription_Insufficient_FILE_FEATURE = "اعتبار تعداد پردازش فایل شما به اتمام رسیده"
18
+ Subscription_Insufficient_OCR_FEATURE = "اعتبار تعداد پردازش صفحات تصویر به متن شما به اتمام رسیده"
19
+ Subscription_Insufficient_TTS_FEATURE = "اعتبار تعداد پردازش متن به گفتار شما به اتمام رسیده"
20
+ Subscription_Insufficient_ASR_FEATURE = "اعتبار تعداد پردازش گفتار به متن شما به اتمام رسیده"
21
+ Subscription_Expired = "اشتراک شما منقضی شده است"
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+ class FeatureDisplayType(str, Enum):
4
+ LINEAR = "linear"
5
+ CIRCULAR = "circular"
@@ -0,0 +1,11 @@
1
+ from enum import Enum
2
+
3
+ class FeatureKey(str, Enum):
4
+ MESSAGES = "messages"
5
+ ASR = "asr"
6
+ TTS = "tts"
7
+ BOTS = "bots"
8
+ FILES = "files"
9
+ URLS = "urls"
10
+ TEXTS = "texts"
11
+ OCR = "ocr"
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+ class OrderType(str, Enum):
4
+ PLAN = "plan"
5
+ FEATURE_TOPUP = "feature_topup"
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ class PlanInterval(str, Enum):
4
+ DAY = 'day'
5
+ WEEK = 'week'
6
+ MONTH = 'month'
7
+ YEAR = 'year'
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ class PlanType(str, Enum):
4
+ NORMAL = 'normal'
5
+ CUSTOM = 'custom'
6
+ FREE = 'free'
7
+ UNLIMITED = "unlimited"
@@ -0,0 +1,33 @@
1
+ from datetime import datetime
2
+ from typing import List, Optional
3
+ from pydantic import BaseModel
4
+ from sharedkernel.objects.base_document import BaseDocument
5
+ from sharedkernel.enum import PlanInterval, OrderType, PlanType, FeatureKey, FeatureDisplayType
6
+
7
+
8
+ class SubscriptionFeature(BaseModel):
9
+ feature_key: FeatureKey
10
+ feature_unit: Optional[str] = None
11
+ remaining: int
12
+ original: int
13
+ title: str
14
+ description: Optional[str] = None
15
+ display_type: FeatureDisplayType = FeatureDisplayType.CIRCULAR
16
+
17
+ class Subscription(BaseDocument):
18
+ order_id: str
19
+ plan_id: str
20
+ plan_title: str
21
+ user_id: str
22
+ workspace_id: str
23
+ interval: PlanInterval
24
+ interval_count: int
25
+ type: OrderType
26
+ plan_type: PlanType | None = PlanType.FREE
27
+ features: List[SubscriptionFeature] | None = []
28
+ paid_at: datetime | None = None
29
+ from_date: datetime | None = None
30
+ to_date: datetime | None = None
31
+ is_active: bool
32
+ sum_messages: int | None = 0
33
+ avg_messages: float | None = 0
@@ -0,0 +1,70 @@
1
+ from typing import Type
2
+ from pydantic import BaseModel
3
+ import aiohttp
4
+
5
+ from sharedkernel.objects.result import Result
6
+ from sharedkernel.enum import FeatureKey, PlanType
7
+ from sharedkernel.enum import ErrorCode
8
+ from sharedkernel.objects.subscription import Subscription
9
+ from sharedkernel.exception.exception import BusinessException
10
+
11
+
12
+ class ToolboxService:
13
+ def __init__(self, cache):
14
+ self.cache = cache
15
+
16
+ async def send_request(
17
+ self,
18
+ method: str,
19
+ endpoint: str,
20
+ payload: dict = None,
21
+ params: dict | None = None,
22
+ headers: dict | None = None,
23
+ model: Type[BaseModel] | None = None,
24
+ ) -> Result:
25
+
26
+ async with aiohttp.ClientSession() as session:
27
+ async with session.request(
28
+ method=method.upper(),
29
+ url=endpoint,
30
+ json=payload,
31
+ params=params,
32
+ headers=headers,
33
+ ) as response:
34
+
35
+ response_data = await response.json()
36
+ return Result(
37
+ **response_data,
38
+ )
39
+
40
+
41
+
42
+ def check_user_subscription(
43
+ self,
44
+ user_subscription: Subscription,
45
+ feature: FeatureKey,
46
+ quantity: int = 1
47
+ ):
48
+
49
+ feature_errors = {
50
+ FeatureKey.MESSAGES: ErrorCode.Subscription_Insufficient_Message_Feature,
51
+ FeatureKey.BOTS: ErrorCode.Subscription_Insufficient_Bot_Feature,
52
+ FeatureKey.URLS: ErrorCode.Subscription_Insufficient_URL_FEATURE,
53
+ FeatureKey.TEXTS: ErrorCode.Subscription_Insufficient_TEXT_FEATURE,
54
+ FeatureKey.FILES: ErrorCode.Subscription_Insufficient_FILE_FEATURE,
55
+ FeatureKey.OCR: ErrorCode.Subscription_Insufficient_OCR_FEATURE,
56
+ FeatureKey.TTS: ErrorCode.Subscription_Insufficient_TTS_FEATURE,
57
+ FeatureKey.ASR: ErrorCode.Subscription_Insufficient_ASR_FEATURE,
58
+ }
59
+
60
+ if not user_subscription:
61
+ raise BusinessException(error_code=ErrorCode.Subscription_Not_Found)
62
+
63
+ if user_subscription.plan_type == PlanType.UNLIMITED:
64
+ return
65
+
66
+ for _feature in user_subscription.features:
67
+ if _feature.feature_key == feature:
68
+ if _feature.remaining - quantity < 0:
69
+ raise BusinessException(error_code=feature_errors[feature])
70
+ break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sharedkernel
3
- Version: 2.7.0
3
+ Version: 2.9.0
4
4
  Summary: sharekernel is a shared package between all python projects
5
5
  Author: Smilinno
6
6
  Description-Content-Type: text/markdown
@@ -21,6 +21,7 @@ Requires-Dist: beautifulsoup4
21
21
  Requires-Dist: deepdiff
22
22
  Requires-Dist: kombu
23
23
  Requires-Dist: redis==8.0.0
24
+ Requires-Dist: aiohttp
24
25
  Dynamic: author
25
26
  Dynamic: description
26
27
  Dynamic: description-content-type
@@ -31,6 +32,10 @@ Dynamic: summary
31
32
  this is a shared kernel package
32
33
 
33
34
  # Change Log
35
+ ### Version 2.9.0
36
+ - Add toolbox_service
37
+ ### Version 2.8.0
38
+ - Add chunker for tts
34
39
  ### Version 2.7.0
35
40
  - Add Redis and cache repository
36
41
  ### Version 2.6.4
@@ -1,5 +1,5 @@
1
1
  sharedkernel/common.py,sha256=HL5vsuJBaIeBcoXA8Hbe6jnYAv4danIszo5Y7G2gGDA,622
2
- sharedkernel/config.py,sha256=H_PjT9jawZqKtwnlVCSuefkh_b24QJX2ckP-YNGp6aA,1996
2
+ sharedkernel/config.py,sha256=63qBvXrd7OYTInWg0g9g4cIRx6wGMoQcyTZjzDtQJ1U,1932
3
3
  sharedkernel/data_format_converter.py,sha256=GWGbfhKJBifkz-cfnqKAFjJM43WC0qdq9KSELj3xR30,3774
4
4
  sharedkernel/date_converter.py,sha256=Cjd4ewm0pIfQzv7nlgAAB_EYrr-VvXxQGehJCNphgXc,4491
5
5
  sharedkernel/diff_utils.py,sha256=mtwJmc05GAXUOB0ZLtqAhfBT1kGoSQ7qmP5N44P73ho,2564
@@ -10,6 +10,9 @@ sharedkernel/multipart_upload.py,sha256=JVlCBlznB9dWh2_spjAqzLOqQT1CHUTvrR4m7ug8
10
10
  sharedkernel/regex_masking.py,sha256=zQrgteP8Cuq1EC9B7QUJqAXUxK9ISD9kWMYK2AbRfw0,3288
11
11
  sharedkernel/s3_uploader.py,sha256=VWgN-RVHmLXMDuxgZux3M-iFwWk5zhRnUIECKS0auW4,3637
12
12
  sharedkernel/string_extentions.py,sha256=ld02W06gd0Ql80GQU6nlqPAeUSfOe2Yr8cCzf3lJgQY,98
13
+ sharedkernel/toolbox_service.py,sha256=l4nPm7DiXCudeQd-JRYyS-T52g7q_YCsTJ9Sz9iDKAY,2440
14
+ sharedkernel/chunker/chunk_rule.py,sha256=djxeQ3UvMx3_suH-heAQB0VwINT7uMfV7rnuuJChmwU,2531
15
+ sharedkernel/chunker/text_chunker.py,sha256=QL8rQjJX-7U9TXMWiQBPldb9h1EeEECmFwLF1JLoGAM,3146
13
16
  sharedkernel/database/__init__.py,sha256=AtIbU7pDKwY6YCg_J8PX62WcYld1lAvBcVFW62MdiFg,241
14
17
  sharedkernel/database/audit_model.py,sha256=SMAYrvMb7XvPi4076TDgkOLaxh_3Jg_tfE12qKOy8RA,364
15
18
  sharedkernel/database/distributed_cache.py,sha256=XRhyK1MVBY-kPjFaAa99XVPE6ofdjBCdbvu_Xdh758k,3945
@@ -19,9 +22,14 @@ sharedkernel/database/mongo_health_checker.py,sha256=IYAaV8mJgGG_uxfnSRRvbq6Jybx
19
22
  sharedkernel/database/pagination_response_dto.py,sha256=91Gkajx4hHmYfypP7EJTiMP1ZrzopjwRuEyMrzQ3SVE,389
20
23
  sharedkernel/database/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
24
  sharedkernel/database/cache/cache_repository.py,sha256=xL5Q18wgsSzIID_Zq8r0sa2OKC_c96q7cg2zUALzPwg,750
22
- sharedkernel/database/cache/redis_generic_cache.py,sha256=9_jUDfvUP37_IQ0ANDzniHv-Hj6lQDKPtJ0J76e8r7A,2526
23
- sharedkernel/enum/__init__.py,sha256=BQxVEO5O1PrNB8gm5nUrh_9Us2HcdgPF-8iZuro7UEM,73
24
- sharedkernel/enum/error_code.py,sha256=M_z-rAW4-zmWs1hMcc97zo4CpqzmoUidMarwo9UuNAk,551
25
+ sharedkernel/database/cache/redis_generic_cache.py,sha256=kJbKeVFjmfgfRnZVhshSuQ21fcL9XSx5rzyqEeCR95w,2521
26
+ sharedkernel/enum/__init__.py,sha256=pO4_p4pSJZWgeXHNej4j_cHGroQdnn46pOyShnh2bBE,273
27
+ sharedkernel/enum/error_code.py,sha256=oVi85wKUxZNpoOQFD7f2j0tHWkNz9fKHbjCCIlqNSMM,1747
28
+ sharedkernel/enum/feature_display_type.py,sha256=aMaLtbUu4vo_efCyRNmVVYIl_UgDwt5N_C4Ghf5YU10,107
29
+ sharedkernel/enum/feature_key.py,sha256=6ZOfnduGlhDjx2fN3AqogQuEdIW4tRwYh04b4MkTUig,201
30
+ sharedkernel/enum/order_type.py,sha256=Aj9p7il4HxM6iM_1QU9Obja6CH_e7j6RnFFRJcjJWbs,104
31
+ sharedkernel/enum/plan_interval.py,sha256=Fn_E6NyswYa00OETE4E8wV8o4gNE7WfP6tsDxYO_fPQ,125
32
+ sharedkernel/enum/plan_type.py,sha256=ExcPl8wKTbRu1gYB7jZqweaj-K1F8HTXlCuxEOIvl3g,139
25
33
  sharedkernel/enum/redis_mode_enum.py,sha256=v_n9g7_rq_pdMiZrC8mMveRcjgF3eBv3K2tsaV1Pidc,136
26
34
  sharedkernel/enum/sort_order.py,sha256=T_1AknuVg2OSJMlIDkEiCUNHPW0yDnFvbzoNpTgG3-I,97
27
35
  sharedkernel/exception/__init__.py,sha256=Yjkd1VsLFuzfWcMj-v8YYTA-vjtUlF4ESglBFeN1zvs,315
@@ -42,8 +50,9 @@ sharedkernel/objects/base_document.py,sha256=TERbRaZaW_zExm7Ddpk_3nCwMvSroGC3Kkd
42
50
  sharedkernel/objects/json_string_model.py,sha256=j63tnoqiok0EmBP6T-ChYuQYKPw7mLqUOHuaCDUZkmw,1333
43
51
  sharedkernel/objects/jwt_model.py,sha256=XQHQhTbg7PT8XiUh5fd9MwRH4ldPsesI_hfbjaSqdKg,134
44
52
  sharedkernel/objects/result.py,sha256=I_9hX5TPEO1oStzuFLjFh1rtimXorz7ml-OaW_2BMvc,680
53
+ sharedkernel/objects/subscription.py,sha256=9MT2Vu-cf8ozO4pwh00ovk6aRNMjbJboKP8NkJgeSRI,1043
45
54
  sharedkernel/objects/user_info.py,sha256=51WyspRxlIWzK7Lfxgqg4D6mylXeHe9ZSenf-RhYTdA,286
46
- sharedkernel-2.7.0.dist-info/METADATA,sha256=8tNBtxeSL5t9rwHqbq94K3xGS3SXVyVjhWHCPZ56fW0,4092
47
- sharedkernel-2.7.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
48
- sharedkernel-2.7.0.dist-info/top_level.txt,sha256=TVTOnV1MItSSlpSjqkiijuHkoVsGHS4CArpsM-lylkE,13
49
- sharedkernel-2.7.0.dist-info/RECORD,,
55
+ sharedkernel-2.9.0.dist-info/METADATA,sha256=twWnN61qrWLNK3QiXFbgzl273OXIfJOp4y0c8O9mEHk,4195
56
+ sharedkernel-2.9.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
57
+ sharedkernel-2.9.0.dist-info/top_level.txt,sha256=TVTOnV1MItSSlpSjqkiijuHkoVsGHS4CArpsM-lylkE,13
58
+ sharedkernel-2.9.0.dist-info/RECORD,,