hypern 0.1.0__cp310-cp310-manylinux_2_34_x86_64.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 (65) hide show
  1. hypern/__init__.py +4 -0
  2. hypern/application.py +234 -0
  3. hypern/auth/__init__.py +0 -0
  4. hypern/auth/authorization.py +2 -0
  5. hypern/background.py +4 -0
  6. hypern/caching/__init__.py +0 -0
  7. hypern/caching/base/__init__.py +8 -0
  8. hypern/caching/base/backend.py +3 -0
  9. hypern/caching/base/key_maker.py +8 -0
  10. hypern/caching/cache_manager.py +56 -0
  11. hypern/caching/cache_tag.py +10 -0
  12. hypern/caching/custom_key_maker.py +11 -0
  13. hypern/caching/redis_backend.py +3 -0
  14. hypern/cli/__init__.py +0 -0
  15. hypern/cli/commands.py +0 -0
  16. hypern/config.py +149 -0
  17. hypern/datastructures.py +27 -0
  18. hypern/db/__init__.py +0 -0
  19. hypern/db/nosql/__init__.py +25 -0
  20. hypern/db/nosql/addons/__init__.py +4 -0
  21. hypern/db/nosql/addons/color.py +16 -0
  22. hypern/db/nosql/addons/daterange.py +30 -0
  23. hypern/db/nosql/addons/encrypted.py +53 -0
  24. hypern/db/nosql/addons/password.py +134 -0
  25. hypern/db/nosql/addons/unicode.py +10 -0
  26. hypern/db/sql/__init__.py +176 -0
  27. hypern/db/sql/addons/__init__.py +14 -0
  28. hypern/db/sql/addons/color.py +15 -0
  29. hypern/db/sql/addons/daterange.py +22 -0
  30. hypern/db/sql/addons/datetime.py +22 -0
  31. hypern/db/sql/addons/encrypted.py +58 -0
  32. hypern/db/sql/addons/password.py +170 -0
  33. hypern/db/sql/addons/ts_vector.py +46 -0
  34. hypern/db/sql/addons/unicode.py +15 -0
  35. hypern/db/sql/repository.py +289 -0
  36. hypern/enum.py +13 -0
  37. hypern/exceptions.py +93 -0
  38. hypern/hypern.cpython-310-x86_64-linux-gnu.so +0 -0
  39. hypern/hypern.pyi +172 -0
  40. hypern/i18n/__init__.py +0 -0
  41. hypern/logging/__init__.py +3 -0
  42. hypern/logging/logger.py +91 -0
  43. hypern/middleware/__init__.py +5 -0
  44. hypern/middleware/base.py +16 -0
  45. hypern/middleware/cors.py +38 -0
  46. hypern/middleware/i18n.py +1 -0
  47. hypern/middleware/limit.py +174 -0
  48. hypern/openapi/__init__.py +5 -0
  49. hypern/openapi/schemas.py +64 -0
  50. hypern/openapi/swagger.py +3 -0
  51. hypern/py.typed +0 -0
  52. hypern/response/__init__.py +3 -0
  53. hypern/response/response.py +134 -0
  54. hypern/routing/__init__.py +4 -0
  55. hypern/routing/dispatcher.py +65 -0
  56. hypern/routing/endpoint.py +27 -0
  57. hypern/routing/parser.py +101 -0
  58. hypern/routing/router.py +279 -0
  59. hypern/scheduler.py +5 -0
  60. hypern/security.py +44 -0
  61. hypern/worker.py +30 -0
  62. hypern-0.1.0.dist-info/METADATA +121 -0
  63. hypern-0.1.0.dist-info/RECORD +65 -0
  64. hypern-0.1.0.dist-info/WHEEL +4 -0
  65. hypern-0.1.0.dist-info/licenses/LICENSE +24 -0
@@ -0,0 +1,134 @@
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
@@ -0,0 +1,10 @@
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
@@ -0,0 +1,176 @@
1
+ # -*- coding: utf-8 -*-
2
+ from datetime import datetime
3
+ from contextvars import ContextVar, Token
4
+ from typing import Union, Optional, Dict
5
+ import traceback
6
+ import threading
7
+
8
+ from robyn import Request, Response
9
+ from sqlalchemy.ext.asyncio import AsyncSession, async_scoped_session, AsyncEngine
10
+ from sqlalchemy.orm import Session, sessionmaker
11
+ from sqlalchemy.sql.expression import Delete, Insert, Update
12
+ from contextlib import asynccontextmanager
13
+ from .repository import Model, PostgresRepository
14
+ from uuid import uuid4
15
+
16
+
17
+ class ContextStore:
18
+ def __init__(self, cleanup_interval: int = 300, max_age: int = 3600):
19
+ """
20
+ Initialize ContextStore with automatic session cleanup.
21
+
22
+ :param cleanup_interval: Interval between cleanup checks (in seconds)
23
+ :param max_age: Maximum age of a session before it's considered expired (in seconds)
24
+ """
25
+ self._session_times: Dict[str, datetime] = {}
26
+ self.session_var = ContextVar("session_id", default=None)
27
+
28
+ self._max_age = max_age
29
+ self._cleanup_interval = cleanup_interval
30
+ self._cleanup_thread: Optional[threading.Thread] = None
31
+ self._stop_event = threading.Event()
32
+
33
+ # Start the cleanup thread
34
+ self._start_cleanup_thread()
35
+
36
+ def _start_cleanup_thread(self):
37
+ """Start a background thread for periodic session cleanup."""
38
+
39
+ def cleanup_worker():
40
+ while not self._stop_event.is_set():
41
+ self._perform_cleanup()
42
+ self._stop_event.wait(self._cleanup_interval)
43
+
44
+ self._cleanup_thread = threading.Thread(
45
+ target=cleanup_worker,
46
+ daemon=True, # Allows the thread to be automatically terminated when the main program exits
47
+ )
48
+ self._cleanup_thread.start()
49
+
50
+ def _perform_cleanup(self):
51
+ """Perform cleanup of expired sessions."""
52
+ current_time = datetime.now()
53
+ expired_sessions = [
54
+ session_id for session_id, timestamp in list(self._session_times.items()) if (current_time - timestamp).total_seconds() > self._max_age
55
+ ]
56
+
57
+ for session_id in expired_sessions:
58
+ self.remove_session(session_id)
59
+
60
+ def remove_session(self, session_id: str):
61
+ """Remove a specific session."""
62
+ self._session_times.pop(session_id, None)
63
+
64
+ def set_context(self, session_id: str):
65
+ """
66
+ Context manager for setting and resetting session context.
67
+
68
+ :param session_id: Unique identifier for the session
69
+ :return: Context manager for session
70
+ """
71
+ self.session_var.set(session_id)
72
+ self._session_times[session_id] = datetime.now()
73
+
74
+ def get_context(self) -> str:
75
+ """
76
+ Get the current session context.
77
+
78
+ :return: Current session ID
79
+ :raises RuntimeError: If no session context is available
80
+ """
81
+ return self.session_var.get()
82
+
83
+ def reset_context(self):
84
+ """Reset the session context."""
85
+ token = self.get_context()
86
+ if token is not None:
87
+ self.session_var.reset(token)
88
+
89
+ def stop_cleanup(self):
90
+ """
91
+ Stop the cleanup thread.
92
+ Useful for graceful shutdown of the application.
93
+ """
94
+ self._stop_event.set()
95
+ if self._cleanup_thread:
96
+ self._cleanup_thread.join()
97
+
98
+ def __del__(self):
99
+ """
100
+ Ensure cleanup thread is stopped when the object is deleted.
101
+ """
102
+ self.stop_cleanup()
103
+
104
+
105
+ class SqlConfig:
106
+ def __init__(self, default_engine: AsyncEngine | None = None, reader_engine: AsyncEngine | None = None, writer_engine: AsyncEngine | None = None):
107
+ """
108
+ Initialize the SQL configuration.
109
+ You can provide a default engine, a reader engine, and a writer engine.
110
+ If only one engine is provided (default_engine), it will be used for both reading and writing.
111
+ If both reader and writer engines are provided, they will be used for reading and writing respectively.
112
+ Note: The reader and writer engines must be different.
113
+ """
114
+
115
+ assert default_engine or reader_engine or writer_engine, "At least one engine must be provided."
116
+ assert not (reader_engine and writer_engine and id(reader_engine) == id(writer_engine)), "Reader and writer engines must be different."
117
+
118
+ engines = {
119
+ "writer": writer_engine or default_engine,
120
+ "reader": reader_engine or default_engine,
121
+ }
122
+ self.session_store = ContextStore()
123
+
124
+ class RoutingSession(Session):
125
+ def get_bind(this, mapper=None, clause=None, **kwargs):
126
+ if this._flushing or isinstance(clause, (Update, Delete, Insert)):
127
+ return engines["writer"].sync_engine
128
+ return engines["reader"].sync_engine
129
+
130
+ async_session_factory = sessionmaker(
131
+ class_=AsyncSession,
132
+ sync_session_class=RoutingSession,
133
+ expire_on_commit=False,
134
+ )
135
+
136
+ session_scope: Union[AsyncSession, async_scoped_session] = async_scoped_session(
137
+ session_factory=async_session_factory,
138
+ scopefunc=self.session_store.get_context,
139
+ )
140
+
141
+ @asynccontextmanager
142
+ async def get_session():
143
+ """
144
+ Get the database session.
145
+ This can be used for dependency injection.
146
+
147
+ :return: The database session.
148
+ """
149
+ try:
150
+ yield session_scope
151
+ except Exception:
152
+ traceback.print_exc()
153
+ await session_scope.rollback()
154
+ finally:
155
+ await session_scope.remove()
156
+ await session_scope.close()
157
+
158
+ self.get_session = get_session
159
+ self._context_token: Optional[Token] = None
160
+
161
+ def before_request(self, request: Request):
162
+ token = str(uuid4())
163
+ self.session_store.set_context(token)
164
+ return request
165
+
166
+ def after_request(self, response: Response):
167
+ self.session_store.reset_context()
168
+ return response
169
+
170
+ def init_app(self, app):
171
+ app.inject_global(get_session=self.get_session)
172
+ app.before_request(endpoint=None)(self.before_request)
173
+ app.after_request(endpoint=None)(self.after_request)
174
+
175
+
176
+ __all__ = ["Model", "PostgresRepository", "SqlConfig"]
@@ -0,0 +1,14 @@
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
+ ]
@@ -0,0 +1,15 @@
1
+ from sqlalchemy.types import TypeDecorator, String
2
+ import re
3
+
4
+
5
+ class ColorField(TypeDecorator):
6
+ impl = String
7
+
8
+ def process_bind_param(self, value, dialect):
9
+ color_regex = r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"
10
+ if not re.match(color_regex, value):
11
+ raise ValueError("Invalid color format. Use hexadecimal color codes (e.g., #FF0000)")
12
+ return value
13
+
14
+ def process_result_value(self, value, dialect):
15
+ return value
@@ -0,0 +1,22 @@
1
+ from sqlalchemy.types import TypeDecorator
2
+ from sqlalchemy.dialects.postgresql import DATERANGE
3
+ from datetime import datetime
4
+
5
+
6
+ class DateRangeField(TypeDecorator):
7
+ impl = DATERANGE
8
+
9
+ def process_bind_param(self, value, dialect):
10
+ if value is None:
11
+ return None
12
+ elif "start" in value and "end" in value:
13
+ return f"['{value['start']}', '{value['end']}']"
14
+ else:
15
+ raise ValueError('DateRangeField must be a dictionary with "start" and "end" keys')
16
+
17
+ def process_result_value(self, value, dialect):
18
+ if value is None:
19
+ return None
20
+ else:
21
+ start, end = value[1:-1].split(",")
22
+ 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")}
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,58 @@
1
+ # -*- coding: utf-8 -*-
2
+ from cryptography.hazmat.primitives import padding
3
+ from sqlalchemy.types import TypeDecorator, LargeBinary, String
4
+
5
+ from hypern.security import EDEngine, AESEngine
6
+
7
+ import os
8
+ import typing
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
@@ -0,0 +1,170 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from sqlalchemy import types
4
+ from sqlalchemy.dialects import oracle, postgresql, sqlite
5
+ from sqlalchemy.ext.mutable import Mutable
6
+ from passlib.context import LazyCryptContext
7
+ import weakref
8
+ import passlib
9
+
10
+
11
+ class Password(Mutable):
12
+ @classmethod
13
+ def coerce(cls, key, value):
14
+ if isinstance(value, Password):
15
+ return value
16
+
17
+ if isinstance(value, (str, bytes)):
18
+ return cls(value, secret=True)
19
+
20
+ super().coerce(key, value)
21
+
22
+ def __init__(self, value, context=None, secret=False):
23
+ # Store the hash (if it is one).
24
+ self.hash = value if not secret else None
25
+
26
+ # Store the secret if we have one.
27
+ self.secret = value if secret else None
28
+
29
+ # The hash should be bytes.
30
+ if isinstance(self.hash, str):
31
+ self.hash = self.hash.encode("utf8")
32
+
33
+ # Save weakref of the password context (if we have one)
34
+ self.context = weakref.proxy(context) if context is not None else None
35
+
36
+ def __eq__(self, value):
37
+ if self.hash is None or value is None:
38
+ # Ensure that we don't continue comparison if one of us is None.
39
+ return self.hash is value
40
+
41
+ if isinstance(value, Password):
42
+ # Comparing 2 hashes isn't very useful; but this equality
43
+ # method breaks otherwise.
44
+ return value.hash == self.hash
45
+
46
+ if self.context is None:
47
+ # Compare 2 hashes again as we don't know how to validate.
48
+ return value == self
49
+
50
+ if isinstance(value, (str, bytes)):
51
+ valid, new = self.context.verify_and_update(value, self.hash)
52
+ if valid and new:
53
+ # New hash was calculated due to various reasons; stored one
54
+ # wasn't optimal, etc.
55
+ self.hash = new
56
+
57
+ # The hash should be bytes.
58
+ if isinstance(self.hash, str):
59
+ self.hash = self.hash.encode("utf8")
60
+ self.changed()
61
+
62
+ return valid
63
+
64
+ return False
65
+
66
+ def __ne__(self, value):
67
+ return self != value
68
+
69
+
70
+ class PasswordType(types.TypeDecorator):
71
+ impl = types.String
72
+ cache_ok = True
73
+
74
+ def __init__(self, max_length=None, **kwargs):
75
+ # Fail if passlib is not found.
76
+ if passlib is None:
77
+ raise ImportError("'passlib' is required to use 'PasswordType'")
78
+
79
+ # Construct the passlib crypt context.
80
+ self.context = LazyCryptContext(**kwargs)
81
+ self._max_length = max_length
82
+
83
+ @property
84
+ def hashing_method(self):
85
+ return "hash" if hasattr(self.context, "hash") else "encrypt"
86
+
87
+ @property
88
+ def max_length(self):
89
+ """Get column length."""
90
+ if self._max_length is None:
91
+ self._max_length = self.calculate_max_length()
92
+
93
+ return self._max_length
94
+
95
+ def calculate_max_length(self):
96
+ # Calculate the largest possible encoded password.
97
+ # name + rounds + salt + hash + ($ * 4) of largest hash
98
+ max_lengths = [1024]
99
+ for name in self.context.schemes():
100
+ scheme = getattr(__import__("passlib.hash").hash, name)
101
+ length = 4 + len(scheme.name)
102
+ length += len(str(getattr(scheme, "max_rounds", "")))
103
+ length += getattr(scheme, "max_salt_size", 0) or 0
104
+ length += getattr(scheme, "encoded_checksum_size", scheme.checksum_size)
105
+ max_lengths.append(length)
106
+
107
+ # Return the maximum calculated max length.
108
+ return max(max_lengths)
109
+
110
+ def load_dialect_impl(self, dialect):
111
+ if dialect.name == "postgresql":
112
+ # Use a BYTEA type for postgresql.
113
+ impl = postgresql.BYTEA(self.max_length)
114
+ elif dialect.name == "oracle":
115
+ # Use a RAW type for oracle.
116
+ impl = oracle.RAW(self.max_length)
117
+ elif dialect.name == "sqlite":
118
+ # Use a BLOB type for sqlite
119
+ impl = sqlite.BLOB(self.max_length)
120
+ else:
121
+ # Use a VARBINARY for all other dialects.
122
+ impl = types.VARBINARY(self.max_length)
123
+ return dialect.type_descriptor(impl)
124
+
125
+ def process_bind_param(self, value, dialect):
126
+ if isinstance(value, Password):
127
+ # If were given a password secret; hash it.
128
+ if value.secret is not None:
129
+ return self._hash(value.secret).encode("utf8")
130
+
131
+ # Value has already been hashed.
132
+ return value.hash
133
+
134
+ if isinstance(value, str):
135
+ # Assume value has not been hashed.
136
+ return self._hash(value).encode("utf8")
137
+
138
+ def process_result_value(self, value, dialect):
139
+ if value is not None:
140
+ return Password(value, self.context)
141
+
142
+ def _hash(self, value):
143
+ return getattr(self.context, self.hashing_method)(value)
144
+
145
+ def _coerce(self, value):
146
+ if value is None:
147
+ return
148
+
149
+ if not isinstance(value, Password):
150
+ # Hash the password using the default scheme.
151
+ value = self._hash(value).encode("utf8")
152
+ return Password(value, context=self.context)
153
+
154
+ else:
155
+ # If were given a password object; ensure the context is right.
156
+ value.context = weakref.proxy(self.context)
157
+
158
+ # If were given a password secret; hash it.
159
+ if value.secret is not None:
160
+ value.hash = self._hash(value.secret).encode("utf8")
161
+ value.secret = None
162
+
163
+ return value
164
+
165
+ @property
166
+ def python_type(self):
167
+ return self.impl.type.python_type
168
+
169
+
170
+ Password.associate_with(PasswordType)
@@ -0,0 +1,46 @@
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__()
@@ -0,0 +1,15 @@
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