hypern 0.3.8__cp312-cp312-win32.whl → 0.3.10__cp312-cp312-win32.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.
Files changed (33) hide show
  1. hypern/application.py +3 -0
  2. hypern/args_parser.py +7 -0
  3. hypern/database/sqlalchemy/__init__.py +4 -0
  4. hypern/database/{addons/sqlalchemy/__init__.py → sqlalchemy/config.py} +0 -5
  5. hypern/database/{sql → sqlx}/query.py +1 -1
  6. hypern/hypern.cp312-win32.pyd +0 -0
  7. hypern/hypern.pyi +1 -0
  8. {hypern-0.3.8.dist-info → hypern-0.3.10.dist-info}/METADATA +2 -9
  9. {hypern-0.3.8.dist-info → hypern-0.3.10.dist-info}/RECORD +16 -32
  10. hypern/database/addons/__init__.py +0 -5
  11. hypern/database/addons/sqlalchemy/fields/__init__.py +0 -14
  12. hypern/database/addons/sqlalchemy/fields/color.py +0 -16
  13. hypern/database/addons/sqlalchemy/fields/daterange.py +0 -23
  14. hypern/database/addons/sqlalchemy/fields/datetime.py +0 -22
  15. hypern/database/addons/sqlalchemy/fields/encrypted.py +0 -58
  16. hypern/database/addons/sqlalchemy/fields/password.py +0 -171
  17. hypern/database/addons/sqlalchemy/fields/ts_vector.py +0 -46
  18. hypern/database/addons/sqlalchemy/fields/unicode.py +0 -15
  19. hypern/database/nosql/__init__.py +0 -25
  20. hypern/database/nosql/addons/__init__.py +0 -4
  21. hypern/database/nosql/addons/color.py +0 -16
  22. hypern/database/nosql/addons/daterange.py +0 -30
  23. hypern/database/nosql/addons/encrypted.py +0 -53
  24. hypern/database/nosql/addons/password.py +0 -134
  25. hypern/database/nosql/addons/unicode.py +0 -10
  26. hypern/security.py +0 -44
  27. /hypern/database/{addons/sqlalchemy → sqlalchemy}/repository.py +0 -0
  28. /hypern/database/{sql → sqlx}/__init__.py +0 -0
  29. /hypern/database/{sql → sqlx}/field.py +0 -0
  30. /hypern/database/{sql → sqlx}/migrate.py +0 -0
  31. /hypern/database/{sql → sqlx}/model.py +0 -0
  32. {hypern-0.3.8.dist-info → hypern-0.3.10.dist-info}/WHEEL +0 -0
  33. {hypern-0.3.8.dist-info → hypern-0.3.10.dist-info}/licenses/LICENSE +0 -0
hypern/application.py CHANGED
@@ -437,6 +437,9 @@ class Hypern:
437
437
  self.args.workers = self.thread_config.workers
438
438
  self.args.max_blocking_threads = self.thread_config.max_blocking_threads
439
439
 
440
+ if self.args.http2:
441
+ server.enable_http2()
442
+
440
443
  run_processes(
441
444
  server=server,
442
445
  host=self.args.host,
hypern/args_parser.py CHANGED
@@ -55,6 +55,12 @@ class ArgsConfig:
55
55
  help="It sets the number of workers and max-blocking-threads automatically.",
56
56
  )
57
57
 
58
+ parser.add_argument(
59
+ "--http2",
60
+ action="store_true",
61
+ help="Enable HTTP/2 support.",
62
+ )
63
+
58
64
  args, _ = parser.parse_known_args()
59
65
 
60
66
  self.host = args.host or "127.0.0.1"
@@ -64,3 +70,4 @@ class ArgsConfig:
64
70
  self.workers = args.workers or 1
65
71
  self.reload = args.reload or False
66
72
  self.auto_workers = args.auto_workers
73
+ self.http2 = args.http2 or False
@@ -0,0 +1,4 @@
1
+ from .repository import Model, PostgresRepository
2
+ from .config import SqlalchemyConfig
3
+
4
+ __all__ = ["Model", "PostgresRepository", "SqlalchemyConfig"]
@@ -7,8 +7,6 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_sessi
7
7
  from sqlalchemy.orm import Session, sessionmaker
8
8
  from sqlalchemy.sql.expression import Delete, Insert, Update
9
9
 
10
- from .repository import Model, PostgresRepository
11
-
12
10
 
13
11
  class SqlalchemyConfig:
14
12
  def __init__(self, default_engine: AsyncEngine | None = None, reader_engine: AsyncEngine | None = None, writer_engine: AsyncEngine | None = None):
@@ -66,6 +64,3 @@ class SqlalchemyConfig:
66
64
 
67
65
  def init_app(self, app):
68
66
  app.inject("get_session", self.get_session)
69
-
70
-
71
- __all__ = ["Model", "PostgresRepository", "SqlalchemyConfig"]
@@ -1,6 +1,6 @@
1
1
  from enum import Enum
2
2
  from typing import Any, Dict, List, Tuple, Union
3
- from hypern.database.sql.field import ForeignKeyField
3
+ from hypern.database.sqlx.field import ForeignKeyField
4
4
 
5
5
 
6
6
  class JoinType(Enum):
Binary file
hypern/hypern.pyi CHANGED
@@ -188,6 +188,7 @@ class Server:
188
188
  def set_shutdown_handler(self, on_shutdown: FunctionInfo) -> None: ...
189
189
  def set_database_config(self, config: DatabaseConfig) -> None: ...
190
190
  def set_dependencies(self, dependencies: Dict[str, Any]) -> None: ...
191
+ def enable_http2(self) -> None: ...
191
192
 
192
193
  class Route:
193
194
  path: str
@@ -1,31 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypern
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
7
7
  Requires-Dist: sqlalchemy[asyncio] ==2.0.31
8
- Requires-Dist: passlib ==1.7.4
9
8
  Requires-Dist: pyjwt ==2.8.0
10
9
  Requires-Dist: pydash ==8.0.3
11
10
  Requires-Dist: sentry-sdk ==2.11.0
12
- Requires-Dist: pydantic-settings ==2.3.4
13
11
  Requires-Dist: celery ==5.4.0
14
- Requires-Dist: asgiref ==3.8.1
15
12
  Requires-Dist: psycopg ==3.2.3
16
13
  Requires-Dist: pyyaml ==6.0.2
17
- Requires-Dist: mongoengine ==0.29.1
18
- Requires-Dist: argon2-cffi ==23.1.0
19
- Requires-Dist: bcrypt ==4.2.0
20
14
  Requires-Dist: orjson ==3.10.11
21
15
  Requires-Dist: multiprocess ==0.70.17
22
16
  Requires-Dist: uvloop ==0.21.0 ; sys_platform != 'win32' and platform_python_implementation == 'CPython' and platform_machine != 'armv7l'
23
- Requires-Dist: cryptography ==43.0.3
24
17
  Requires-Dist: watchdog ==6.0.0
25
- Requires-Dist: jsonschema ==4.23.0
26
18
  Requires-Dist: psutil ==6.1.0
27
19
  Requires-Dist: msgpack ==1.1.0
28
20
  Requires-Dist: redis ==5.2.1
21
+ Requires-Dist: pydantic ==2.10.4
29
22
  License-File: LICENSE
30
23
  Summary: A Fast Async Python backend with a Rust runtime.
31
24
  Author-email: Martin Dang <vannghiem848@gmail.com>
@@ -1,8 +1,8 @@
1
- hypern-0.3.8.dist-info/METADATA,sha256=0bMLw-pA0VeoGBbQxXr4cqCO8dS6esga5NjluEeV5MY,4356
2
- hypern-0.3.8.dist-info/WHEEL,sha256=19xj5Waw2omQTyAh5Pnrm7rXeZkfzX1OuBYghlKYN-I,92
3
- hypern-0.3.8.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
- hypern/application.py,sha256=07QGDd3w7tQRL8njfbNVinG6l8W7O0V-rN_SkO5O3B4,18002
5
- hypern/args_parser.py,sha256=BjGCooHdZjOpyN-AGL9PCKCKdzROVThE3dzQgC97rng,2035
1
+ hypern-0.3.10.dist-info/METADATA,sha256=qForGiEDhs-zr3H_hrC_I7E5xDe4Otsl1yFLGAFeZJ0,4113
2
+ hypern-0.3.10.dist-info/WHEEL,sha256=19xj5Waw2omQTyAh5Pnrm7rXeZkfzX1OuBYghlKYN-I,92
3
+ hypern-0.3.10.dist-info/licenses/LICENSE,sha256=qbYKAIJLS6jYg5hYncKE7OtWmqOtpVTvKNkwOa0Iwwg,1328
4
+ hypern/application.py,sha256=EVa3bjUjgEmM7jDpGTVcFlBa7kk2m69QMlpdsp8AWpU,18068
5
+ hypern/args_parser.py,sha256=1v41RZT-35iY80I5OugjeFPXPr28HUvniQwu-29y1vw,2222
6
6
  hypern/auth/authorization.py,sha256=-NprZsI0np889ZN1fp-MiVFrPoMNzUtatBJaCMtkllM,32
7
7
  hypern/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  hypern/background.py,sha256=xy38nQZSJsYFRXr3-uFJeNW9E1GiXXOC7lSe5pC0eyE,124
@@ -13,29 +13,14 @@ hypern/caching/__init__.py,sha256=ODO7zMm4iFG8wcvrhKmukryG5wOTW0DnzFvNMfF57Cc,35
13
13
  hypern/cli/commands.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  hypern/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  hypern/config.py,sha256=Jij9eGg5NgC8Un5Lw5i7ghuEMAfkVctdcoE4RaN5LTE,8157
16
- hypern/database/addons/sqlalchemy/fields/color.py,sha256=Jj8q4lkT0ukKyjVyZjBx7fokGrX7AIJpKOGzwsBpfnU,480
17
- hypern/database/addons/sqlalchemy/fields/daterange.py,sha256=qEfQN9c4jQdzeXNYKgQ4VIJ3Qc0HXHWRouzNF1se-RA,853
18
- hypern/database/addons/sqlalchemy/fields/datetime.py,sha256=Bp2jMja2lb_b2WnzRnfbjXXTHBgBTyph1ECsItIwvvg,643
19
- hypern/database/addons/sqlalchemy/fields/encrypted.py,sha256=pXsg4ImPpK-VkLDruKrv2gUtdp2kajQX7xDbRDxa-SI,1930
20
- hypern/database/addons/sqlalchemy/fields/password.py,sha256=9pypORygWaINj3oiAOBRIOGgpuA0lcjPq4rh1pqJxq0,5789
21
- hypern/database/addons/sqlalchemy/fields/ts_vector.py,sha256=bQyXYvQ1bAfFpYcS-sFwM7fU6L1lg9_7nGDRGp0CoUo,1361
22
- hypern/database/addons/sqlalchemy/fields/unicode.py,sha256=rbqyHlsPUqRDWIjYQSRFF3zkHncnwxo0sdlzqUlcrUw,411
23
- hypern/database/addons/sqlalchemy/fields/__init__.py,sha256=mLN_AvwgpSAbrWZvVHZuO7ff0gk1T_JbVwd5wug5nlw,359
24
- hypern/database/addons/sqlalchemy/repository.py,sha256=ue6vWOTrnEPyDevlyh3v-7PU6GSfrZHYKrbXVuoS8UA,9516
25
- hypern/database/addons/sqlalchemy/__init__.py,sha256=FuY78ubEwtifdQTVHhCrscYaAarlp2urgYBc_R77yt0,2766
26
- hypern/database/addons/__init__.py,sha256=mdW0P0xvnK8htUk02ujvIaeHXl6w53JjrTS4ioNi1Bw,63
27
- hypern/database/nosql/addons/color.py,sha256=bAGRuARCAYwZ1nO4jK0lzGYKmavTDtS34BxvrsetF74,446
28
- hypern/database/nosql/addons/daterange.py,sha256=hGUSoVFqatNY-TB5wjZTq62iZpHpdsyRJIsHxsj1uDs,1192
29
- hypern/database/nosql/addons/encrypted.py,sha256=B0M-uDqvZHVmIZcFdwcuC2MGsv0pGJFQ1lrOg8klR9U,1741
30
- hypern/database/nosql/addons/password.py,sha256=jfZxvWFm6nV9EWpXq5Mj-jpqnl9QbokZj9WT14n7dKE,5035
31
- hypern/database/nosql/addons/unicode.py,sha256=LaDpLfdoTcJuASPE-8fqOVD05H_uOx8gOdnyDn5Iu0c,268
32
- hypern/database/nosql/addons/__init__.py,sha256=WEtPM8sPHilvga7zxwqvINeTkF0hdcfgPcAnHc4MASE,125
33
- hypern/database/nosql/__init__.py,sha256=MH9YvlbRlbBCrQVNOdfTaK-hINwJxbJLmxwY9Mei7I8,644
34
- hypern/database/sql/field.py,sha256=gV9u_BvMIoxoDT3_J7sL5XJNa5XFsAO9w324ThwHbNs,9121
35
- hypern/database/sql/migrate.py,sha256=BTtAs3-iMyMDzIWl6B3rM9sj7XGggLDRjD0h_WgGPtc,9742
36
- hypern/database/sql/model.py,sha256=C8_rJA1Adw1yPWthjmAGh26hjTBuwwlEdtH45ADxvL0,4044
37
- hypern/database/sql/query.py,sha256=En19t27zt6iUDQbFgO_wLEWPQCkPeBuH3s37fzlhMVc,33345
38
- hypern/database/sql/__init__.py,sha256=dbSAz2nP0DPKK4Bb_jJdObSaSYQfgZ8D4U1TJdc4e7c,645
16
+ hypern/database/sqlalchemy/config.py,sha256=3BKY8YoHKEvwdDulp1que6pqmyyzX3CgfrQHtDHZWeE,2646
17
+ hypern/database/sqlalchemy/repository.py,sha256=ue6vWOTrnEPyDevlyh3v-7PU6GSfrZHYKrbXVuoS8UA,9516
18
+ hypern/database/sqlalchemy/__init__.py,sha256=xOMrzdPyJBzwaZifxYg2XD3TSorCOBJOIKYbnbESTuo,154
19
+ hypern/database/sqlx/field.py,sha256=gV9u_BvMIoxoDT3_J7sL5XJNa5XFsAO9w324ThwHbNs,9121
20
+ hypern/database/sqlx/migrate.py,sha256=BTtAs3-iMyMDzIWl6B3rM9sj7XGggLDRjD0h_WgGPtc,9742
21
+ hypern/database/sqlx/model.py,sha256=C8_rJA1Adw1yPWthjmAGh26hjTBuwwlEdtH45ADxvL0,4044
22
+ hypern/database/sqlx/query.py,sha256=db4gKmw_Lvud2xUe97Kec886vLVMRkjoqFasSHxZMiA,33346
23
+ hypern/database/sqlx/__init__.py,sha256=dbSAz2nP0DPKK4Bb_jJdObSaSYQfgZ8D4U1TJdc4e7c,645
39
24
  hypern/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
25
  hypern/datastructures.py,sha256=Pxr9KsZZTFfp0KC1-A4v5AkQfmrUyvVwxKuToQUOLoE,882
41
26
  hypern/enum.py,sha256=KcVziJj7vWvyie0r2rtxhrLzdtkZAsf0DY58oJ4tQl4,360
@@ -50,7 +35,7 @@ hypern/gateway/gateway.py,sha256=26K2qvJUR-0JnN4IlhwvSSt7EYcpYrBVDuzZ1ivQQ34,147
50
35
  hypern/gateway/proxy.py,sha256=w1wcTplDnVrfjn7hb0M0yBVth5TGl88irF-MUYHysQQ,2463
51
36
  hypern/gateway/service.py,sha256=PkRaM08olqM_j_4wRjEJCR8X8ZysAF2WOcfhWjaX2eo,1701
52
37
  hypern/gateway/__init__.py,sha256=TpFWtqnJerW1-jCWq5fjypJcw9Y6ytyrkvkzby1Eg0E,235
53
- hypern/hypern.pyi,sha256=Urc3uHhPupsN-RRYluyWiw8B_xYWzE91Og3Zft40NgU,9010
38
+ hypern/hypern.pyi,sha256=C_RAN4E9m4FFGGTd3k6JVi59ORl_knLQtIHM1wqDlBg,9051
54
39
  hypern/i18n/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
40
  hypern/logging/logger.py,sha256=WACam_IJiCMXX0hGVKMGSxUQpY4DgAXy7M1dD3q-Z9s,3256
56
41
  hypern/logging/__init__.py,sha256=6eVriyncsJ4J73fGYhoejv9MX7aGTkRezTpPxO4DX1I,52
@@ -77,7 +62,6 @@ hypern/routing/queue.py,sha256=NtFBbogU22ddyyX-CuQMip1XFDPZdMCVMIeUCQ-CR6Y,7176
77
62
  hypern/routing/route.py,sha256=kan47-UeL-OPwcpp0rEhmBaaum6hN7FUj13Y8pZDEYA,10256
78
63
  hypern/routing/__init__.py,sha256=U4xW5fDRsn03z4cVLT4dJHHGGU6SVxyv2DL86LXodeE,162
79
64
  hypern/scheduler.py,sha256=-k3tW2AGCnHYSthKXk-FOs_SCtWp3yIxQzwzUJMJsbo,67
80
- hypern/security.py,sha256=3E86Yp_eOSVa1emUvBrDgoF0Sn6eNX0CfLnt87w5CPI,1773
81
65
  hypern/worker.py,sha256=ksJW8jWQg3HbIYnIZ5qdAmO-yh5hLpwvTT3dKkHR4Eo,9761
82
66
  hypern/ws/channel.py,sha256=0ns2qmeoFJOpGLXS_hqldhywDQm_DxHwj6KloQx4Q3I,3183
83
67
  hypern/ws/heartbeat.py,sha256=sWMXzQm6cbDHHA2NHc-gFjv7G_E56XtxswHQ93_BueM,2861
@@ -85,5 +69,5 @@ hypern/ws/room.py,sha256=0_L6Nun0n007F0rfNY8yX5x_A8EuXuI67JqpMkJ4RNI,2598
85
69
  hypern/ws/route.py,sha256=fGQ2RC708MPOiiIHPUo8aZ-oK379TTAyQYm4htNA5jM,803
86
70
  hypern/ws/__init__.py,sha256=dhRoRY683_rfPfSPM5qUczfTuyYDeuLOCFxY4hIdKt8,131
87
71
  hypern/__init__.py,sha256=p3AtJQbsPs1RYEiN1thxH-k5UP8FfLeiJSoP0Vt3MDg,639
88
- hypern/hypern.cp312-win32.pyd,sha256=rvh5yYWRidA1p4JK6cneYi7yjaJ8okvTNHUIpwn6c9k,7591936
89
- hypern-0.3.8.dist-info/RECORD,,
72
+ hypern/hypern.cp312-win32.pyd,sha256=vIv05Fu6jD72f8YzNQcW2C--drkzTBYvmy68E1Be3DI,8263680
73
+ hypern-0.3.10.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- from . import sqlalchemy
2
-
3
- __all__ = [
4
- "sqlalchemy",
5
- ]
@@ -1,14 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from .ts_vector import TSVector
3
- from .datetime import DatetimeType
4
- from .password import PasswordType
5
- from .encrypted import StringEncryptType, LargeBinaryEncryptType, AESEngine
6
-
7
- __all__ = [
8
- "TSVector",
9
- "DatetimeType",
10
- "PasswordType",
11
- "StringEncryptType",
12
- "LargeBinaryEncryptType",
13
- "AESEngine",
14
- ]
@@ -1,16 +0,0 @@
1
- import re
2
-
3
- from sqlalchemy.types import String, TypeDecorator
4
-
5
-
6
- class ColorField(TypeDecorator):
7
- impl = String
8
-
9
- def process_bind_param(self, value, dialect):
10
- color_regex = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
11
- if not re.match(color_regex, value):
12
- raise ValueError("Invalid color format. Use hexadecimal color codes (e.g., #FF0000)")
13
- return value
14
-
15
- def process_result_value(self, value, dialect):
16
- return value
@@ -1,23 +0,0 @@
1
- from datetime import datetime
2
-
3
- from sqlalchemy.dialects.postgresql import DATERANGE
4
- from sqlalchemy.types import TypeDecorator
5
-
6
-
7
- class DateRangeField(TypeDecorator):
8
- impl = DATERANGE
9
-
10
- def process_bind_param(self, value, dialect):
11
- if value is None:
12
- return None
13
- elif "start" in value and "end" in value:
14
- return f"['{value['start']}', '{value['end']}']"
15
- else:
16
- raise ValueError('DateRangeField must be a dictionary with "start" and "end" keys')
17
-
18
- def process_result_value(self, value, dialect):
19
- if value is None:
20
- return None
21
- else:
22
- start, end = value[1:-1].split(",")
23
- return {"start": datetime.strptime(start.strip("'"), "%Y-%m-%d %H:%M:%S.%f"), "end": datetime.strptime(end.strip("'"), "%Y-%m-%d %H:%M:%S.%f")}
@@ -1,22 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from sqlalchemy import types
3
-
4
-
5
- class DatetimeType(types.TypeDecorator):
6
- impl = types.DateTime
7
- cache_ok = True
8
-
9
- def load_dialect_impl(self, dialect):
10
- if dialect.name == "sqlite":
11
- return dialect.type_descriptor(types.TEXT)
12
- return dialect.type_descriptor(self.impl)
13
-
14
- def process_bind_param(self, value, dialect):
15
- if dialect.name == "sqlite":
16
- return value.isoformat()
17
- return value
18
-
19
- def process_result_value(self, value, dialect):
20
- if dialect.name != "sqlite":
21
- return value.timestamp()
22
- return value
@@ -1,58 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import os
3
- import typing
4
-
5
- from cryptography.hazmat.primitives import padding
6
- from sqlalchemy.types import LargeBinary, String, TypeDecorator
7
-
8
- from hypern.security import AESEngine, EDEngine
9
-
10
-
11
- class StringEncryptType(TypeDecorator):
12
- impl = String
13
- cache_ok = True
14
-
15
- def __init__(self, engine: typing.Optional[EDEngine] = None, *args, **kwargs) -> None:
16
- super().__init__(*args, **kwargs)
17
-
18
- if not engine:
19
- key = os.urandom(32)
20
- iv = os.urandom(16)
21
- padding_class = padding.PKCS7
22
- self.engine = AESEngine(secret_key=key, iv=iv, padding_class=padding_class)
23
- else:
24
- self.engine = engine # type: ignore
25
-
26
- def process_bind_param(self, value, dialect):
27
- if value is None:
28
- return value
29
- if not isinstance(value, str):
30
- raise ValueError("Value String Encrypt Type must be a string")
31
- return self.engine.encrypt(value).decode(encoding="utf-8")
32
-
33
- def process_result_value(self, value, dialect):
34
- if value is None:
35
- return value
36
- return self.engine.decrypt(value)
37
-
38
-
39
- class LargeBinaryEncryptType(StringEncryptType):
40
- impl = LargeBinary
41
- cache_ok = True
42
-
43
- def __init__(self, engine: typing.Optional[EDEngine] = None, *args, **kwargs) -> None:
44
- super().__init__(engine=engine, *args, **kwargs) # type: ignore
45
-
46
- def process_bind_param(self, value, dialect):
47
- if value is None:
48
- return value
49
- value = super().process_bind_param(value, dialect)
50
- if isinstance(value, str):
51
- return value.encode("utf-8")
52
- return value
53
-
54
- def process_result_value(self, value, dialect):
55
- if isinstance(value, bytes):
56
- value = value.decode("utf-8")
57
- return super().process_result_value(value, dialect)
58
- return value
@@ -1,171 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import weakref
4
-
5
- import passlib
6
- from passlib.context import LazyCryptContext
7
- from sqlalchemy import types
8
- from sqlalchemy.dialects import oracle, postgresql, sqlite
9
- from sqlalchemy.ext.mutable import Mutable
10
-
11
-
12
- class Password(Mutable):
13
- @classmethod
14
- def coerce(cls, key, value):
15
- if isinstance(value, Password):
16
- return value
17
-
18
- if isinstance(value, (str, bytes)):
19
- return cls(value, secret=True)
20
-
21
- super().coerce(key, value)
22
-
23
- def __init__(self, value, context=None, secret=False):
24
- # Store the hash (if it is one).
25
- self.hash = value if not secret else None
26
-
27
- # Store the secret if we have one.
28
- self.secret = value if secret else None
29
-
30
- # The hash should be bytes.
31
- if isinstance(self.hash, str):
32
- self.hash = self.hash.encode("utf8")
33
-
34
- # Save weakref of the password context (if we have one)
35
- self.context = weakref.proxy(context) if context is not None else None
36
-
37
- def __eq__(self, value):
38
- if self.hash is None or value is None:
39
- # Ensure that we don't continue comparison if one of us is None.
40
- return self.hash is value
41
-
42
- if isinstance(value, Password):
43
- # Comparing 2 hashes isn't very useful; but this equality
44
- # method breaks otherwise.
45
- return value.hash == self.hash
46
-
47
- if self.context is None:
48
- # Compare 2 hashes again as we don't know how to validate.
49
- return value == self
50
-
51
- if isinstance(value, (str, bytes)):
52
- valid, new = self.context.verify_and_update(value, self.hash)
53
- if valid and new:
54
- # New hash was calculated due to various reasons; stored one
55
- # wasn't optimal, etc.
56
- self.hash = new
57
-
58
- # The hash should be bytes.
59
- if isinstance(self.hash, str):
60
- self.hash = self.hash.encode("utf8")
61
- self.changed()
62
-
63
- return valid
64
-
65
- return False
66
-
67
- def __ne__(self, value):
68
- return self != value
69
-
70
-
71
- class PasswordType(types.TypeDecorator):
72
- impl = types.String
73
- cache_ok = True
74
-
75
- def __init__(self, max_length=None, **kwargs):
76
- # Fail if passlib is not found.
77
- if passlib is None:
78
- raise ImportError("'passlib' is required to use 'PasswordType'")
79
-
80
- # Construct the passlib crypt context.
81
- self.context = LazyCryptContext(**kwargs)
82
- self._max_length = max_length
83
-
84
- @property
85
- def hashing_method(self):
86
- return "hash" if hasattr(self.context, "hash") else "encrypt"
87
-
88
- @property
89
- def max_length(self):
90
- """Get column length."""
91
- if self._max_length is None:
92
- self._max_length = self.calculate_max_length()
93
-
94
- return self._max_length
95
-
96
- def calculate_max_length(self):
97
- # Calculate the largest possible encoded password.
98
- # name + rounds + salt + hash + ($ * 4) of largest hash
99
- max_lengths = [1024]
100
- for name in self.context.schemes():
101
- scheme = getattr(__import__("passlib.hash").hash, name)
102
- length = 4 + len(scheme.name)
103
- length += len(str(getattr(scheme, "max_rounds", "")))
104
- length += getattr(scheme, "max_salt_size", 0) or 0
105
- length += getattr(scheme, "encoded_checksum_size", scheme.checksum_size)
106
- max_lengths.append(length)
107
-
108
- # Return the maximum calculated max length.
109
- return max(max_lengths)
110
-
111
- def load_dialect_impl(self, dialect):
112
- if dialect.name == "postgresql":
113
- # Use a BYTEA type for postgresql.
114
- impl = postgresql.BYTEA(self.max_length)
115
- elif dialect.name == "oracle":
116
- # Use a RAW type for oracle.
117
- impl = oracle.RAW(self.max_length)
118
- elif dialect.name == "sqlite":
119
- # Use a BLOB type for sqlite
120
- impl = sqlite.BLOB(self.max_length)
121
- else:
122
- # Use a VARBINARY for all other dialects.
123
- impl = types.VARBINARY(self.max_length)
124
- return dialect.type_descriptor(impl)
125
-
126
- def process_bind_param(self, value, dialect):
127
- if isinstance(value, Password):
128
- # If were given a password secret; hash it.
129
- if value.secret is not None:
130
- return self._hash(value.secret).encode("utf8")
131
-
132
- # Value has already been hashed.
133
- return value.hash
134
-
135
- if isinstance(value, str):
136
- # Assume value has not been hashed.
137
- return self._hash(value).encode("utf8")
138
-
139
- def process_result_value(self, value, dialect):
140
- if value is not None:
141
- return Password(value, self.context)
142
-
143
- def _hash(self, value):
144
- return getattr(self.context, self.hashing_method)(value)
145
-
146
- def _coerce(self, value):
147
- if value is None:
148
- return
149
-
150
- if not isinstance(value, Password):
151
- # Hash the password using the default scheme.
152
- value = self._hash(value).encode("utf8")
153
- return Password(value, context=self.context)
154
-
155
- else:
156
- # If were given a password object; ensure the context is right.
157
- value.context = weakref.proxy(self.context)
158
-
159
- # If were given a password secret; hash it.
160
- if value.secret is not None:
161
- value.hash = self._hash(value.secret).encode("utf8")
162
- value.secret = None
163
-
164
- return value
165
-
166
- @property
167
- def python_type(self):
168
- return self.impl.type.python_type
169
-
170
-
171
- Password.associate_with(PasswordType)
@@ -1,46 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import sqlalchemy as sa
4
- from sqlalchemy.dialects.postgresql import TSVECTOR
5
-
6
-
7
- class TSVector(sa.types.TypeDecorator):
8
- """
9
- .. _TSVECTOR:
10
- https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#full-text-search
11
-
12
-
13
- class IndexModel(Model):
14
- ....
15
- search_vector = Column(
16
- TSVector(),
17
- Computed(
18
- "to_tsvector('english', some_text|| ' ' ||some_text)",
19
- persisted=True,
20
- ),
21
- )
22
- session.query(IndexModel).filter(IndexModel.search_vector.match('foo'))
23
-
24
- session.query(IndexModel).filter(
25
- (IndexModel.name_vector | IndexModel.content_vector).match('foo')
26
- )
27
-
28
- """
29
-
30
- impl = TSVECTOR
31
- cache_ok = True
32
-
33
- class comparator_factory(TSVECTOR.Comparator):
34
- def match(self, other, **kwargs):
35
- if "postgresql_regconfig" not in kwargs:
36
- if "regconfig" in self.type.options:
37
- kwargs["postgresql_regconfig"] = self.type.options["regconfig"]
38
- return TSVECTOR.Comparator.match(self, other, **kwargs)
39
-
40
- def __or__(self, other):
41
- return self.op("||")(other)
42
-
43
- def __init__(self, *args, **kwargs):
44
- self.columns = args
45
- self.options = kwargs
46
- super(TSVector, self).__init__()
@@ -1,15 +0,0 @@
1
- from sqlalchemy.types import TypeDecorator, Unicode
2
-
3
-
4
- class UnicodeField(TypeDecorator):
5
- impl = Unicode
6
-
7
- def process_bind_param(self, value, dialect):
8
- try:
9
- value.encode("utf-8")
10
- except UnicodeEncodeError:
11
- raise ValueError("Value must be valid Unicode")
12
- return value
13
-
14
- def process_result_value(self, value, dialect):
15
- return value
@@ -1,25 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from typing import List, TypedDict
3
-
4
- from uuid import uuid4
5
- from mongoengine import connect
6
-
7
-
8
- class TypedDictModel(TypedDict):
9
- host: str
10
- alias: str
11
-
12
-
13
- class NoSqlConfig:
14
- def __init__(self, dbs_config: List[TypedDictModel]):
15
- self.dbs_config = dbs_config
16
-
17
- def _connect_db(self, db_config: TypedDictModel):
18
- _alias = db_config.get("alias", str(uuid4()))
19
- connect(host=db_config["host"], alias=_alias)
20
-
21
- def init_app(self, app):
22
- self.app = app # noqa
23
- # connect
24
- for db_config in self.dbs_config:
25
- self._connect_db(db_config)
@@ -1,4 +0,0 @@
1
- from .encrypted import EncryptedField
2
- from .password import PasswordField
3
-
4
- __all__ = ["EncryptedField", "PasswordField"]
@@ -1,16 +0,0 @@
1
- from mongoengine import BaseField
2
- import re
3
-
4
-
5
- class ColorField(BaseField):
6
- def validate(self, value):
7
- color_regex = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
8
- if not re.match(color_regex, value):
9
- self.error("Invalid color format. Use hexadecimal color codes (e.g., #FF0000)")
10
- return True
11
-
12
- def to_mongo(self, value):
13
- return value
14
-
15
- def to_python(self, value):
16
- return value
@@ -1,30 +0,0 @@
1
- from mongoengine import BaseField
2
- from datetime import datetime
3
-
4
-
5
- class DateRangeField(BaseField):
6
- def __init__(self, **kwargs):
7
- super(DateRangeField, self).__init__(**kwargs)
8
-
9
- def validate(self, value):
10
- if not isinstance(value, dict) or "start" not in value or "end" not in value:
11
- self.error('DateRangeField must be a dictionary with "start" and "end" keys')
12
- # Use get to safely access keys
13
- start = value.get("start")
14
- end = value.get("end")
15
- # Check if both "start" and "end" are present
16
- if start is None or end is None:
17
- self.error('DateRangeField must contain both "start" and "end" keys')
18
-
19
- # Check if "start" and "end" are datetime objects
20
- if not isinstance(value["start"], datetime) or not isinstance(value["end"], datetime):
21
- self.error('DateRangeField "start" and "end" must be datetime objects')
22
- if value["start"] > value["end"]:
23
- self.error('DateRangeField "start" must be earlier than "end"')
24
- return True
25
-
26
- def to_mongo(self, value):
27
- return value
28
-
29
- def to_python(self, value):
30
- return value
@@ -1,53 +0,0 @@
1
- import os
2
- from typing import Any, Optional
3
- from mongoengine.base import BaseField
4
-
5
- from cryptography.hazmat.primitives import padding
6
-
7
- from hypern.security import EDEngine, AESEngine
8
-
9
-
10
- class EncryptedField(BaseField):
11
- """
12
- A custom MongoEngine field that encrypts data using AES-256-CBC.
13
-
14
- The field automatically handles encryption when saving to MongoDB and
15
- decryption when retrieving data.
16
-
17
- Attributes:
18
- engine: Encryption engine to use. If not provided, will use AES-256-CBC
19
- """
20
-
21
- def __init__(self, engine: Optional[EDEngine] = None, **kwargs):
22
- if not engine:
23
- key = os.urandom(32)
24
- iv = os.urandom(16)
25
- padding_class = padding.PKCS7
26
- self.engine = AESEngine(secret_key=key, iv=iv, padding_class=padding_class)
27
- else:
28
- self.engine = engine # type: ignore
29
- super(EncryptedField, self).__init__(**kwargs)
30
-
31
- def to_mongo(self, value: Any) -> Optional[str]:
32
- """Convert a Python object to a MongoDB-compatible format."""
33
- if value is None:
34
- return None
35
- return self.engine.encrypt(value)
36
-
37
- def to_python(self, value: Optional[str]) -> Optional[str]:
38
- """Convert a MongoDB-compatible format to a Python object."""
39
- if value is None:
40
- return None
41
- if isinstance(value, bytes):
42
- return self.engine.decrypt(value)
43
- return value
44
-
45
- def prepare_query_value(self, op, value: Any) -> Optional[str]:
46
- """Prepare a value used in a query."""
47
- if value is None:
48
- return None
49
-
50
- if op in ("set", "upsert"):
51
- return self.to_mongo(value)
52
-
53
- return value
@@ -1,134 +0,0 @@
1
- from mongoengine.base import BaseField
2
- import weakref
3
- from passlib.context import CryptContext
4
- import re
5
- from typing import Optional, Any
6
-
7
-
8
- class PasswordField(BaseField):
9
- """
10
- A custom password field using passlib for hashing and weakref for reference management.
11
- Supports multiple hashing schemes and automatic upgrade of hash algorithms.
12
- """
13
-
14
- # Class-level password context - shared across all instances
15
- pwd_context = CryptContext(
16
- # List of hashing schemes in order of preference
17
- schemes=["argon2", "pbkdf2_sha256", "bcrypt_sha256"],
18
- # Mark argon2 as default
19
- default="argon2",
20
- # Argon2 parameters
21
- argon2__rounds=4,
22
- argon2__memory_cost=65536,
23
- argon2__parallelism=2,
24
- # PBKDF2 parameters
25
- pbkdf2_sha256__rounds=29000,
26
- )
27
-
28
- def __init__(
29
- self,
30
- min_length: int = 8,
31
- require_number: bool = False,
32
- require_special: bool = False,
33
- require_uppercase: bool = False,
34
- require_lowercase: bool = False,
35
- **kwargs,
36
- ):
37
- """
38
- Initialize the password field with validation rules.
39
-
40
- Args:
41
- min_length: Minimum password length
42
- require_number: Require at least one number
43
- require_special: Require at least one special character
44
- require_uppercase: Require at least one uppercase letter
45
- require_lowercase: Require at least one lowercase letter
46
- """
47
- self.min_length = min_length
48
- self.require_number = require_number
49
- self.require_special = require_special
50
- self.require_uppercase = require_uppercase
51
- self.require_lowercase = require_lowercase
52
-
53
- # Use weakref to store references to parent documents
54
- self.instances = weakref.WeakKeyDictionary()
55
-
56
- kwargs["required"] = True
57
- super(PasswordField, self).__init__(**kwargs)
58
-
59
- def validate_password(self, password: str) -> tuple[bool, str]:
60
- """Validate password strength."""
61
-
62
- if len(password) < self.min_length:
63
- return False, f"Password must be at least {self.min_length} characters long"
64
-
65
- if self.require_number and not re.search(r"\d", password):
66
- return False, "Password must contain at least one number"
67
-
68
- if self.require_special and not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
69
- return False, "Password must contain at least one special character"
70
-
71
- if self.require_uppercase and not re.search(r"[A-Z]", password):
72
- return False, "Password must contain at least one uppercase letter"
73
-
74
- if self.require_lowercase and not re.search(r"[a-z]", password):
75
- return False, "Password must contain at least one lowercase letter"
76
-
77
- return True, ""
78
-
79
- def hash_password(self, password: str) -> str:
80
- """Hash password using the configured passlib context."""
81
- return self.pwd_context.hash(password)
82
-
83
- def verify_password(self, password: str, hash: str) -> tuple[bool, Optional[str]]:
84
- """
85
- Verify password and return tuple of (is_valid, new_hash).
86
- new_hash is provided if the hash needs to be upgraded.
87
- """
88
- try:
89
- is_valid = self.pwd_context.verify(password, hash)
90
- # Check if the hash needs to be upgraded
91
- if is_valid and self.pwd_context.needs_update(hash):
92
- return True, self.hash_password(password)
93
- return is_valid, None
94
- except Exception:
95
- return False, None
96
-
97
- def __get__(self, instance, owner):
98
- """Custom getter using weakref."""
99
- if instance is None:
100
- return self
101
- return self.instances.get(instance)
102
-
103
- def __set__(self, instance, value):
104
- """Custom setter using weakref."""
105
- if value and isinstance(value, str):
106
- # Validate and hash new password
107
- is_valid, error = self.validate_password(value)
108
- if not is_valid:
109
- raise ValueError(error)
110
- hashed = self.hash_password(value)
111
- self.instances[instance] = hashed
112
- instance._data[self.name] = hashed
113
- else:
114
- # If it's already hashed or None
115
- self.instances[instance] = value
116
- instance._data[self.name] = value
117
-
118
- def to_mongo(self, value: str) -> Optional[str]:
119
- """Convert to MongoDB-compatible value."""
120
- if value is None:
121
- return None
122
- return self.hash_password(value)
123
-
124
- def to_python(self, value: str) -> str:
125
- """Convert from MongoDB to Python."""
126
- return value
127
-
128
- def prepare_query_value(self, op, value: Any) -> Optional[str]:
129
- """Prepare value for database operations."""
130
- if value is None:
131
- return None
132
- if op == "exact":
133
- return self.hash_password(value)
134
- return value
@@ -1,10 +0,0 @@
1
- from mongoengine import StringField
2
-
3
-
4
- class UnicodeField(StringField):
5
- def validate(self, value):
6
- try:
7
- value.encode("utf-8")
8
- except UnicodeEncodeError:
9
- self.error("Value must be valid Unicode")
10
- return True
hypern/security.py DELETED
@@ -1,44 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from abc import ABC, abstractmethod
3
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
4
- from cryptography.hazmat.backends import default_backend
5
- from base64 import b64encode, b64decode
6
-
7
- import typing
8
-
9
-
10
- class EDEngine(ABC):
11
- @abstractmethod
12
- def encrypt(self, data: str) -> str:
13
- raise NotImplementedError("Method not implemented")
14
-
15
- @abstractmethod
16
- def decrypt(self, data: str) -> str:
17
- raise NotImplementedError("Method not implemented")
18
-
19
-
20
- class AESEngine(EDEngine):
21
- def __init__(self, secret_key: bytes, iv: bytes, padding_class: typing.Type) -> None:
22
- super().__init__()
23
- self.secret_key = secret_key
24
- self.iv = iv
25
- self.padding = padding_class(128)
26
-
27
- def encrypt(self, data: str) -> bytes:
28
- bytes_data = data.encode("utf-8")
29
- encryptor = Cipher(algorithms.AES(self.secret_key), modes.GCM(self.iv), backend=default_backend()).encryptor()
30
- padder = self.padding.padder()
31
- padded_data = padder.update(bytes_data) + padder.finalize()
32
- enctyped_data = encryptor.update(padded_data) + encryptor.finalize()
33
- tag = encryptor.tag
34
- return b64encode(tag + enctyped_data)
35
-
36
- def decrypt(self, data: bytes) -> str:
37
- data = b64decode(data)
38
- tag = data[:16]
39
- encrypted_data = data[16:]
40
- decryptor = Cipher(algorithms.AES(self.secret_key), modes.GCM(self.iv, tag), backend=default_backend()).decryptor()
41
- unpadder = self.padding.unpadder()
42
- decrypted_data = decryptor.update(encrypted_data) + decryptor.finalize()
43
- unpadded_data = unpadder.update(decrypted_data) + unpadder.finalize()
44
- return unpadded_data.decode("utf-8")
File without changes
File without changes
File without changes
File without changes