howler-api 3.0.0.dev374__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.

Potentially problematic release.


This version of howler-api might be problematic. Click here for more details.

Files changed (198) hide show
  1. howler/__init__.py +0 -0
  2. howler/actions/__init__.py +168 -0
  3. howler/actions/add_label.py +111 -0
  4. howler/actions/add_to_bundle.py +159 -0
  5. howler/actions/change_field.py +76 -0
  6. howler/actions/demote.py +160 -0
  7. howler/actions/example_plugin.py +104 -0
  8. howler/actions/prioritization.py +93 -0
  9. howler/actions/promote.py +147 -0
  10. howler/actions/remove_from_bundle.py +133 -0
  11. howler/actions/remove_label.py +111 -0
  12. howler/actions/transition.py +200 -0
  13. howler/api/__init__.py +249 -0
  14. howler/api/base.py +88 -0
  15. howler/api/socket.py +114 -0
  16. howler/api/v1/__init__.py +97 -0
  17. howler/api/v1/action.py +372 -0
  18. howler/api/v1/analytic.py +748 -0
  19. howler/api/v1/auth.py +382 -0
  20. howler/api/v1/clue.py +99 -0
  21. howler/api/v1/configs.py +58 -0
  22. howler/api/v1/dossier.py +222 -0
  23. howler/api/v1/help.py +28 -0
  24. howler/api/v1/hit.py +1181 -0
  25. howler/api/v1/notebook.py +82 -0
  26. howler/api/v1/overview.py +191 -0
  27. howler/api/v1/search.py +788 -0
  28. howler/api/v1/template.py +206 -0
  29. howler/api/v1/tool.py +183 -0
  30. howler/api/v1/user.py +416 -0
  31. howler/api/v1/utils/__init__.py +0 -0
  32. howler/api/v1/utils/etag.py +84 -0
  33. howler/api/v1/view.py +288 -0
  34. howler/app.py +235 -0
  35. howler/common/README.md +125 -0
  36. howler/common/__init__.py +0 -0
  37. howler/common/classification.py +979 -0
  38. howler/common/classification.yml +107 -0
  39. howler/common/exceptions.py +167 -0
  40. howler/common/loader.py +154 -0
  41. howler/common/logging/__init__.py +241 -0
  42. howler/common/logging/audit.py +138 -0
  43. howler/common/logging/format.py +38 -0
  44. howler/common/net.py +79 -0
  45. howler/common/net_static.py +1494 -0
  46. howler/common/random_user.py +316 -0
  47. howler/common/swagger.py +117 -0
  48. howler/config.py +64 -0
  49. howler/cronjobs/__init__.py +29 -0
  50. howler/cronjobs/retention.py +61 -0
  51. howler/cronjobs/rules.py +274 -0
  52. howler/cronjobs/view_cleanup.py +88 -0
  53. howler/datastore/README.md +112 -0
  54. howler/datastore/__init__.py +0 -0
  55. howler/datastore/bulk.py +72 -0
  56. howler/datastore/collection.py +2342 -0
  57. howler/datastore/constants.py +119 -0
  58. howler/datastore/exceptions.py +41 -0
  59. howler/datastore/howler_store.py +105 -0
  60. howler/datastore/migrations/fix_process.py +41 -0
  61. howler/datastore/operations.py +130 -0
  62. howler/datastore/schemas.py +90 -0
  63. howler/datastore/store.py +231 -0
  64. howler/datastore/support/__init__.py +0 -0
  65. howler/datastore/support/build.py +215 -0
  66. howler/datastore/support/schemas.py +90 -0
  67. howler/datastore/types.py +22 -0
  68. howler/error.py +91 -0
  69. howler/external/__init__.py +0 -0
  70. howler/external/generate_mitre.py +96 -0
  71. howler/external/generate_sigma_rules.py +31 -0
  72. howler/external/generate_tlds.py +47 -0
  73. howler/external/reindex_data.py +66 -0
  74. howler/external/wipe_databases.py +58 -0
  75. howler/gunicorn_config.py +25 -0
  76. howler/healthz.py +47 -0
  77. howler/helper/__init__.py +0 -0
  78. howler/helper/azure.py +50 -0
  79. howler/helper/discover.py +59 -0
  80. howler/helper/hit.py +236 -0
  81. howler/helper/oauth.py +247 -0
  82. howler/helper/search.py +92 -0
  83. howler/helper/workflow.py +110 -0
  84. howler/helper/ws.py +378 -0
  85. howler/odm/README.md +102 -0
  86. howler/odm/__init__.py +1 -0
  87. howler/odm/base.py +1543 -0
  88. howler/odm/charter.txt +146 -0
  89. howler/odm/helper.py +416 -0
  90. howler/odm/howler_enum.py +25 -0
  91. howler/odm/models/__init__.py +0 -0
  92. howler/odm/models/action.py +33 -0
  93. howler/odm/models/analytic.py +90 -0
  94. howler/odm/models/assemblyline.py +48 -0
  95. howler/odm/models/aws.py +23 -0
  96. howler/odm/models/azure.py +16 -0
  97. howler/odm/models/cbs.py +44 -0
  98. howler/odm/models/config.py +558 -0
  99. howler/odm/models/dossier.py +33 -0
  100. howler/odm/models/ecs/__init__.py +0 -0
  101. howler/odm/models/ecs/agent.py +17 -0
  102. howler/odm/models/ecs/autonomous_system.py +16 -0
  103. howler/odm/models/ecs/client.py +149 -0
  104. howler/odm/models/ecs/cloud.py +141 -0
  105. howler/odm/models/ecs/code_signature.py +27 -0
  106. howler/odm/models/ecs/container.py +32 -0
  107. howler/odm/models/ecs/dns.py +62 -0
  108. howler/odm/models/ecs/egress.py +10 -0
  109. howler/odm/models/ecs/elf.py +74 -0
  110. howler/odm/models/ecs/email.py +122 -0
  111. howler/odm/models/ecs/error.py +14 -0
  112. howler/odm/models/ecs/event.py +140 -0
  113. howler/odm/models/ecs/faas.py +24 -0
  114. howler/odm/models/ecs/file.py +84 -0
  115. howler/odm/models/ecs/geo.py +30 -0
  116. howler/odm/models/ecs/group.py +18 -0
  117. howler/odm/models/ecs/hash.py +16 -0
  118. howler/odm/models/ecs/host.py +17 -0
  119. howler/odm/models/ecs/http.py +37 -0
  120. howler/odm/models/ecs/ingress.py +12 -0
  121. howler/odm/models/ecs/interface.py +21 -0
  122. howler/odm/models/ecs/network.py +30 -0
  123. howler/odm/models/ecs/observer.py +45 -0
  124. howler/odm/models/ecs/organization.py +12 -0
  125. howler/odm/models/ecs/os.py +21 -0
  126. howler/odm/models/ecs/pe.py +17 -0
  127. howler/odm/models/ecs/process.py +216 -0
  128. howler/odm/models/ecs/registry.py +26 -0
  129. howler/odm/models/ecs/related.py +45 -0
  130. howler/odm/models/ecs/rule.py +51 -0
  131. howler/odm/models/ecs/server.py +24 -0
  132. howler/odm/models/ecs/threat.py +247 -0
  133. howler/odm/models/ecs/tls.py +58 -0
  134. howler/odm/models/ecs/url.py +51 -0
  135. howler/odm/models/ecs/user.py +57 -0
  136. howler/odm/models/ecs/user_agent.py +20 -0
  137. howler/odm/models/ecs/vulnerability.py +41 -0
  138. howler/odm/models/gcp.py +16 -0
  139. howler/odm/models/hit.py +356 -0
  140. howler/odm/models/howler_data.py +328 -0
  141. howler/odm/models/lead.py +24 -0
  142. howler/odm/models/localized_label.py +13 -0
  143. howler/odm/models/overview.py +16 -0
  144. howler/odm/models/pivot.py +40 -0
  145. howler/odm/models/template.py +24 -0
  146. howler/odm/models/user.py +83 -0
  147. howler/odm/models/view.py +34 -0
  148. howler/odm/random_data.py +888 -0
  149. howler/odm/randomizer.py +609 -0
  150. howler/patched.py +5 -0
  151. howler/plugins/__init__.py +25 -0
  152. howler/plugins/config.py +123 -0
  153. howler/remote/__init__.py +0 -0
  154. howler/remote/datatypes/README.md +355 -0
  155. howler/remote/datatypes/__init__.py +98 -0
  156. howler/remote/datatypes/counters.py +63 -0
  157. howler/remote/datatypes/events.py +66 -0
  158. howler/remote/datatypes/hash.py +206 -0
  159. howler/remote/datatypes/lock.py +42 -0
  160. howler/remote/datatypes/queues/__init__.py +0 -0
  161. howler/remote/datatypes/queues/comms.py +59 -0
  162. howler/remote/datatypes/queues/multi.py +32 -0
  163. howler/remote/datatypes/queues/named.py +93 -0
  164. howler/remote/datatypes/queues/priority.py +215 -0
  165. howler/remote/datatypes/set.py +118 -0
  166. howler/remote/datatypes/user_quota_tracker.py +54 -0
  167. howler/security/__init__.py +253 -0
  168. howler/security/socket.py +108 -0
  169. howler/security/utils.py +185 -0
  170. howler/services/__init__.py +0 -0
  171. howler/services/action_service.py +111 -0
  172. howler/services/analytic_service.py +128 -0
  173. howler/services/auth_service.py +323 -0
  174. howler/services/config_service.py +128 -0
  175. howler/services/dossier_service.py +252 -0
  176. howler/services/event_service.py +93 -0
  177. howler/services/hit_service.py +893 -0
  178. howler/services/jwt_service.py +158 -0
  179. howler/services/lucene_service.py +286 -0
  180. howler/services/notebook_service.py +119 -0
  181. howler/services/overview_service.py +44 -0
  182. howler/services/template_service.py +45 -0
  183. howler/services/user_service.py +331 -0
  184. howler/utils/__init__.py +0 -0
  185. howler/utils/annotations.py +28 -0
  186. howler/utils/chunk.py +38 -0
  187. howler/utils/dict_utils.py +200 -0
  188. howler/utils/isotime.py +17 -0
  189. howler/utils/list_utils.py +11 -0
  190. howler/utils/lucene.py +77 -0
  191. howler/utils/path.py +27 -0
  192. howler/utils/socket_utils.py +61 -0
  193. howler/utils/str_utils.py +256 -0
  194. howler/utils/uid.py +47 -0
  195. howler_api-3.0.0.dev374.dist-info/METADATA +71 -0
  196. howler_api-3.0.0.dev374.dist-info/RECORD +198 -0
  197. howler_api-3.0.0.dev374.dist-info/WHEEL +4 -0
  198. howler_api-3.0.0.dev374.dist-info/entry_points.txt +8 -0
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from typing import TYPE_CHECKING, Generic, Optional, TypeVar, Union
6
+
7
+ from howler.common.exceptions import HowlerValueError
8
+ from howler.remote.datatypes import get_client, retry_call
9
+
10
+ if TYPE_CHECKING:
11
+ from redis import Redis
12
+
13
+
14
+ _conditional_remove_script = """
15
+ local hash_name = KEYS[1]
16
+ local key_in_hash = ARGV[1]
17
+ local expected_value = ARGV[2]
18
+ local result = redis.call('hget', hash_name, key_in_hash)
19
+ if result == expected_value then
20
+ redis.call('hdel', hash_name, key_in_hash)
21
+ return 1
22
+ end
23
+ return 0
24
+ """
25
+
26
+
27
+ h_pop_script = """
28
+ local result = redis.call('hget', ARGV[1], ARGV[2])
29
+ if result then redis.call('hdel', ARGV[1], ARGV[2]) end
30
+ return result
31
+ """
32
+
33
+
34
+ _limited_add = """
35
+ local set_name = KEYS[1]
36
+ local key = ARGV[1]
37
+ local value = ARGV[2]
38
+ local limit = tonumber(ARGV[3])
39
+
40
+ if redis.call('hlen', set_name) < limit then
41
+ return redis.call('hsetnx', set_name, key, value)
42
+ end
43
+ return nil
44
+ """
45
+
46
+ T = TypeVar("T")
47
+
48
+
49
+ class HashIterator(Generic[T]):
50
+ def __init__(self, hash_object: Hash[T]):
51
+ self.hash_object = hash_object
52
+ self.cursor = 0
53
+ self.buffer: list[tuple[str, T]] = []
54
+ self._load_next()
55
+
56
+ def __next__(self) -> tuple[str, T]:
57
+ while True:
58
+ if self.buffer:
59
+ return self.buffer.pop(0)
60
+ if self.cursor == 0:
61
+ raise StopIteration()
62
+ self._load_next()
63
+
64
+ def _load_next(self):
65
+ self.cursor, data = retry_call(self.hash_object.c.hscan, self.hash_object.name, self.cursor)
66
+ for key, value in data.items():
67
+ self.buffer.append((key.decode("utf-8"), json.loads(value)))
68
+
69
+
70
+ class Hash(Generic[T]):
71
+ def __init__(
72
+ self,
73
+ name: str,
74
+ host: Optional[Union[str, Redis]] = None,
75
+ port: Optional[int] = None,
76
+ ):
77
+ self.c = get_client(host, port, False)
78
+ self.name = name
79
+ self._pop = self.c.register_script(h_pop_script)
80
+ self._limited_add = self.c.register_script(_limited_add)
81
+ self._conditional_remove = self.c.register_script(_conditional_remove_script)
82
+
83
+ def __iter__(self):
84
+ return HashIterator(self)
85
+
86
+ def __enter__(self):
87
+ return self
88
+
89
+ def __exit__(self, exc_type, exc_val, exc_tb):
90
+ self.delete()
91
+
92
+ def add(self, key: str, value: T) -> int:
93
+ """Add the (key, value) pair to the hash for new keys.
94
+
95
+ If a key already exists this operation doesn't add it.
96
+
97
+ Returns:
98
+ True if key has been added to the table, False otherwise.
99
+ """
100
+ if isinstance(key, bytes):
101
+ raise HowlerValueError("Cannot use bytes for hashmap keys")
102
+ return retry_call(self.c.hsetnx, self.name, key, json.dumps(value))
103
+
104
+ def increment(self, key, increment: int = 1):
105
+ return int(retry_call(self.c.hincrby, self.name, key, increment))
106
+
107
+ def limited_add(self, key, value, size_limit):
108
+ """Add a single value to the set, but only if that wouldn't make the set grow past a given size.
109
+
110
+ If the hash has hit the size limit returns None
111
+ Otherwise, returns the result of hsetnx (same as `add`)
112
+ """
113
+ return retry_call(
114
+ self._limited_add,
115
+ keys=[self.name],
116
+ args=[key, json.dumps(value), size_limit],
117
+ )
118
+
119
+ def exists(self, key: str) -> bool:
120
+ return retry_call(self.c.hexists, self.name, key)
121
+
122
+ def get(self, key: str) -> Optional[T]:
123
+ item = retry_call(self.c.hget, self.name, key)
124
+ if not item:
125
+ return item
126
+ return json.loads(item)
127
+
128
+ def keys(self) -> list[str]:
129
+ return [k.decode("utf-8") for k in retry_call(self.c.hkeys, self.name)]
130
+
131
+ def length(self):
132
+ return retry_call(self.c.hlen, self.name)
133
+
134
+ def items(self) -> dict[str, T]:
135
+ items = retry_call(self.c.hgetall, self.name)
136
+ if not isinstance(items, dict):
137
+ return {}
138
+ return {k.decode("utf-8"): json.loads(v) for k, v in items.items()}
139
+
140
+ def conditional_remove(self, key: str, value) -> bool:
141
+ return bool(
142
+ retry_call(
143
+ self._conditional_remove,
144
+ keys=[self.name],
145
+ args=[key, json.dumps(value)],
146
+ )
147
+ )
148
+
149
+ def pop(self, key: str):
150
+ item = retry_call(self._pop, args=[self.name, key])
151
+ if not item:
152
+ return item
153
+ return json.loads(item)
154
+
155
+ def set(self, key: str, value: T):
156
+ if isinstance(key, bytes):
157
+ raise HowlerValueError("Cannot use bytes for hashmap keys")
158
+ return retry_call(self.c.hset, self.name, key, json.dumps(value))
159
+
160
+ def multi_set(self, data: dict[str, T]):
161
+ if any(isinstance(key, bytes) for key in data.keys()):
162
+ raise HowlerValueError("Cannot use bytes for hashmap keys")
163
+ encoded = {key: json.dumps(value) for key, value in data.items()}
164
+ return retry_call(self.c.hset, self.name, mapping=encoded)
165
+
166
+ def delete(self):
167
+ retry_call(self.c.delete, self.name)
168
+
169
+
170
+ class ExpiringHash(Hash):
171
+ def __init__(self, name, ttl=86400, host=None, port=None):
172
+ super(ExpiringHash, self).__init__(name, host, port)
173
+ self.ttl = ttl
174
+ self.last_expire_time: float = 0
175
+
176
+ def _conditional_expire(self):
177
+ if self.ttl:
178
+ ctime = time.time()
179
+ if ctime > self.last_expire_time + (self.ttl / 2):
180
+ retry_call(self.c.expire, self.name, self.ttl)
181
+ self.last_expire_time = ctime
182
+
183
+ def add(self, key, value):
184
+ rval = super(ExpiringHash, self).add(key, value)
185
+ self._conditional_expire()
186
+ return rval
187
+
188
+ def set(self, key, value):
189
+ rval = super(ExpiringHash, self).set(key, value)
190
+ self._conditional_expire()
191
+ return rval
192
+
193
+ def multi_set(self, data):
194
+ rval = super(ExpiringHash, self).multi_set(data)
195
+ self._conditional_expire()
196
+ return rval
197
+
198
+ def increment(self, key, increment=1):
199
+ rval = super(ExpiringHash, self).increment(key, increment)
200
+ self._conditional_expire()
201
+ return rval
202
+
203
+ def limited_add(self, key, value, size_limit):
204
+ rval = super(ExpiringHash, self).limited_add(key, value, size_limit)
205
+ self._conditional_expire()
206
+ return rval
@@ -0,0 +1,42 @@
1
+ from howler.remote.datatypes import get_client, retry_call
2
+ from howler.utils.uid import get_random_id
3
+
4
+ lock_acquire_script = """
5
+ local lock_holder = ARGV[1]
6
+ local uuid = ARGV[2]
7
+ local timeout = ARGV[3]
8
+ if redis.call('setnx', lock_holder, uuid) == 1 then
9
+ redis.call('expire', lock_holder, timeout)
10
+ return true
11
+ end
12
+ return false
13
+ """
14
+
15
+ lock_release_script = """
16
+ local lock_holder = ARGV[1]
17
+ local lock_release = ARGV[2]
18
+ local uuid = ARGV[3]
19
+ if redis.call('get', lock_holder) == uuid then
20
+ redis.call('del', lock_holder)
21
+ redis.call('rpush', lock_release, uuid)
22
+ redis.call('expire', lock_release, 1)
23
+ end
24
+ """
25
+
26
+
27
+ class Lock(object):
28
+ def __init__(self, name, timeout, host=None, port=None):
29
+ self.uuid = get_random_id()
30
+ self.c = get_client(host, port, False)
31
+ self.lock_release = "-".join(("lock", str(timeout), name, "released"))
32
+ self.lock_holder = "-".join(("lock", str(timeout), name, "holder"))
33
+ self.timeout = timeout
34
+ self._acquire = self.c.register_script(lock_acquire_script)
35
+ self._release = self.c.register_script(lock_release_script)
36
+
37
+ def __enter__(self):
38
+ while not retry_call(self._acquire, args=[self.lock_holder, self.uuid, self.timeout]):
39
+ retry_call(self.c.blpop, self.lock_release, 1)
40
+
41
+ def __exit__(self, unused1, unused2, unused3):
42
+ retry_call(self._release, args=[self.lock_holder, self.lock_release, self.uuid])
File without changes
@@ -0,0 +1,59 @@
1
+ import json
2
+
3
+ import redis
4
+
5
+ from howler.remote.datatypes import decode, get_client, log, retry_call
6
+
7
+
8
+ class CommsQueue(object):
9
+ def __init__(self, names, host=None, port=None, private=False):
10
+ self.c = get_client(host, port, private)
11
+ self.p = retry_call(self.c.pubsub)
12
+ if not isinstance(names, list):
13
+ names = [names]
14
+ self.names = names
15
+ self._connected = False
16
+
17
+ def __enter__(self):
18
+ return self
19
+
20
+ def __exit__(self, exc_type, exc_val, exc_tb):
21
+ retry_call(self.p.unsubscribe)
22
+
23
+ def _connect(self):
24
+ if not self._connected:
25
+ retry_call(self.p.subscribe, self.names)
26
+ self._connected = True
27
+
28
+ def close(self):
29
+ retry_call(self.p.close)
30
+
31
+ def listen(self, blocking=True):
32
+ retried = False
33
+ while True:
34
+ self._connect()
35
+ try:
36
+ if blocking:
37
+ i = self.p.listen()
38
+ v = next(i)
39
+ else:
40
+ v = self.p.get_message()
41
+ if v is None:
42
+ yield None
43
+ continue
44
+
45
+ if isinstance(v, dict) and v.get("type", None) == "message":
46
+ data = decode(v.get("data", "null"))
47
+ yield data
48
+ except redis.ConnectionError:
49
+ log.warning("No connection to Redis, reconnecting...")
50
+ self._connected = False
51
+ retried = True
52
+ finally:
53
+ if self._connected and retried:
54
+ log.info("Reconnected to Redis!")
55
+ retried = False
56
+
57
+ def publish(self, message):
58
+ for name in self.names:
59
+ retry_call(self.c.publish, name, json.dumps(message))
@@ -0,0 +1,32 @@
1
+ import json
2
+
3
+ from howler.remote.datatypes import get_client, retry_call
4
+
5
+
6
+ class MultiQueue(object):
7
+ def __init__(self, host=None, port=None, private=False):
8
+ self.c = get_client(host, port, private)
9
+
10
+ def delete(self, name):
11
+ retry_call(self.c.delete, name)
12
+
13
+ def length(self, name):
14
+ return retry_call(self.c.llen, name)
15
+
16
+ def pop(self, name, blocking=True, timeout=0):
17
+ if blocking:
18
+ response = retry_call(self.c.blpop, name, timeout)
19
+ else:
20
+ response = retry_call(self.c.lpop, name)
21
+
22
+ if not response:
23
+ return response
24
+
25
+ if blocking:
26
+ return json.loads(response[1])
27
+ else:
28
+ return json.loads(response)
29
+
30
+ def push(self, name, *messages):
31
+ for message in messages:
32
+ retry_call(self.c.rpush, name, json.dumps(message))
@@ -0,0 +1,93 @@
1
+ import json
2
+ import time
3
+ from typing import Generic, Optional, TypeVar
4
+
5
+ from howler.common.exceptions import HowlerTypeError
6
+ from howler.remote.datatypes import get_client, retry_call
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class NamedQueue(Generic[T]):
12
+ def __init__(self, name: str, host=None, port=None, private: bool = False, ttl: int = 0):
13
+ self.c = get_client(host, port, private)
14
+ self.name: str = name
15
+ self.ttl: int = ttl
16
+ self.last_expire_time: float = 0
17
+
18
+ def __enter__(self):
19
+ return self
20
+
21
+ def __exit__(self, exc_type, exc_val, exc_tb):
22
+ self.delete()
23
+
24
+ def _conditional_expire(self):
25
+ if self.ttl:
26
+ ctime = time.time()
27
+ if ctime > self.last_expire_time + (self.ttl / 2):
28
+ retry_call(self.c.expire, self.name, self.ttl)
29
+ self.last_expire_time = ctime
30
+
31
+ def delete(self):
32
+ retry_call(self.c.delete, self.name)
33
+
34
+ def __len__(self):
35
+ return self.length()
36
+
37
+ def length(self):
38
+ return retry_call(self.c.llen, self.name)
39
+
40
+ def peek_next(self) -> Optional[T]:
41
+ response = retry_call(self.c.lrange, self.name, 0, 0)
42
+
43
+ if response:
44
+ return json.loads(response[0])
45
+ return None
46
+
47
+ def pop_batch(self, size) -> list[T]:
48
+ response = retry_call(self.c.lpop, self.name, size)
49
+
50
+ if not response:
51
+ return []
52
+ return [json.loads(r) for r in response]
53
+
54
+ def pop(self, blocking: bool = True, timeout: int = 0) -> Optional[T]:
55
+ if blocking:
56
+ response = retry_call(self.c.blpop, self.name, timeout)
57
+ else:
58
+ response = retry_call(self.c.lpop, self.name)
59
+
60
+ if not response:
61
+ return response
62
+
63
+ if blocking:
64
+ return json.loads(response[1])
65
+ else:
66
+ return json.loads(response)
67
+
68
+ def push(self, *messages: T):
69
+ for message in messages:
70
+ retry_call(self.c.rpush, self.name, json.dumps(message))
71
+ self._conditional_expire()
72
+
73
+ def unpop(self, *messages: T):
74
+ """Put all messages passed back at the head of the FIFO queue."""
75
+ for message in messages:
76
+ retry_call(self.c.lpush, self.name, json.dumps(message))
77
+ self._conditional_expire()
78
+
79
+
80
+ def select(*queues, **kw):
81
+ timeout = kw.get("timeout", 0)
82
+ if len(queues) < 1:
83
+ raise HowlerTypeError("At least one queue must be specified")
84
+ if any([not isinstance(q, NamedQueue) for q in queues]):
85
+ raise HowlerTypeError("Only NamedQueues supported")
86
+
87
+ c = queues[0].c
88
+ response = retry_call(c.blpop, [q.name for q in queues], timeout)
89
+
90
+ if not response:
91
+ return response
92
+
93
+ return response[0].decode("utf-8"), json.loads(response[1])
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from typing import Generic, Optional, TypeVar, Union
6
+
7
+ from howler.common.exceptions import HowlerTypeError
8
+ from howler.remote.datatypes import decode, get_client, retry_call
9
+
10
+ SORTING_KEY_LEN = 21
11
+
12
+ # Work around for inconsistency between ZRANGEBYSCORE and ZREMRANGEBYSCORE
13
+ # (No limit option available or we would just be using that directly)
14
+ #
15
+ # args:
16
+ # minimum score to pop
17
+ # maximum score to pop
18
+ # number of elements to skip before popping any
19
+ # max element count to pop
20
+ pq_dequeue_range_script = """
21
+ local unpack = table.unpack or unpack
22
+ local min_score = tonumber(ARGV[1]);
23
+ if min_score == nil then min_score = -math.huge end
24
+ local max_score = tonumber(ARGV[2]);
25
+ if max_score == nil then max_score = math.huge end
26
+ local rem_offset = tonumber(ARGV[3]);
27
+ local rem_limit = tonumber(ARGV[4]);
28
+
29
+ local entries = redis.call("zrangebyscore", KEYS[1], -max_score, -min_score, "limit", rem_offset, rem_limit);
30
+ if #entries > 0 then redis.call("zrem", KEYS[1], unpack(entries)) end
31
+ return entries
32
+ """
33
+
34
+ T = TypeVar("T")
35
+
36
+
37
+ class PriorityQueue(Generic[T]):
38
+ def __init__(self, name, host=None, port=None, private=False):
39
+ self.c = get_client(host, port, private)
40
+ self._deque_range = self.c.register_script(pq_dequeue_range_script)
41
+ self.name = name
42
+
43
+ def __enter__(self):
44
+ return self
45
+
46
+ def __exit__(self, exc_type, exc_val, exc_tb):
47
+ self.delete()
48
+
49
+ def count(self, lowest, highest):
50
+ return retry_call(self.c.zcount, self.name, -highest, -lowest)
51
+
52
+ def delete(self):
53
+ retry_call(self.c.delete, self.name)
54
+
55
+ def length(self):
56
+ return retry_call(self.c.zcard, self.name)
57
+
58
+ def pop(self, num=None):
59
+ if num is not None and num <= 0:
60
+ return []
61
+
62
+ if num:
63
+ return [decode(s[0][SORTING_KEY_LEN:]) for s in retry_call(self.c.zpopmin, self.name, num)]
64
+ else:
65
+ ret_val = retry_call(self.c.zpopmin, self.name, 1)
66
+ if ret_val:
67
+ return decode(ret_val[0][0][SORTING_KEY_LEN:])
68
+ return None
69
+
70
+ def blocking_pop(self, timeout=0, low_priority=False):
71
+ """When only one item is requested, blocking is is possible."""
72
+ if low_priority:
73
+ result = retry_call(self.c.bzpopmax, self.name, timeout)
74
+ else:
75
+ result = retry_call(self.c.bzpopmin, self.name, timeout)
76
+ if result:
77
+ return decode(result[1][SORTING_KEY_LEN:])
78
+ return None
79
+
80
+ def dequeue_range(self, lower_limit="", upper_limit="", skip=0, num=1) -> list[T]:
81
+ """Dequeue a number of elements, within a specified range of scores.
82
+
83
+ Limits given are inclusive, can be made exclusive, see redis docs on how to format limits for that.
84
+
85
+ NOTE: lower/upper limit is negated+swapped in the lua script, no need to do it here
86
+
87
+ :param lower_limit: The score of all dequeued elements must be higher or equal to this.
88
+ :param upper_limit: The score of all dequeued elements must be lower or equal to this.
89
+ :param skip: In the range of available items to dequeue skip over this many.
90
+ :param num: Maximum number of elements to dequeue.
91
+ :return: list
92
+ """
93
+ results = retry_call(
94
+ self._deque_range,
95
+ keys=[self.name],
96
+ args=[lower_limit, upper_limit, skip, num],
97
+ )
98
+ return [decode(res[SORTING_KEY_LEN:]) for res in results]
99
+
100
+ def push(self, priority: int, data: T, vip=None):
101
+ vip = 0 if vip else 9
102
+ value = f"{vip}{f'{int(time.time()*1000000):020}'}{json.dumps(data)}"
103
+ retry_call(self.c.zadd, self.name, {value: -priority})
104
+ return value
105
+
106
+ def rank(self, raw_value):
107
+ return retry_call(self.c.zrank, self.name, raw_value)
108
+
109
+ def remove(self, raw_value):
110
+ return retry_call(self.c.zrem, self.name, raw_value)
111
+
112
+ def unpush(self, num=None) -> Union[list[T], Optional[T]]:
113
+ if num is not None and num <= 0:
114
+ return []
115
+
116
+ if num:
117
+ return [decode(s[0][SORTING_KEY_LEN:]) for s in retry_call(self.c.zpopmax, self.name, num)]
118
+ else:
119
+ ret_val = retry_call(self.c.zpopmax, self.name, 1)
120
+ if ret_val:
121
+ return decode(ret_val[0][0][SORTING_KEY_LEN:])
122
+ return None
123
+
124
+
125
+ class UniquePriorityQueue(PriorityQueue):
126
+ """A priority queue where duplicate entries will be merged."""
127
+
128
+ def __init__(self, name, host=None, port=None, private=False):
129
+ super().__init__(name, host, port, private)
130
+
131
+ def remove(self, data: str):
132
+ """Remove a value from the priority queue."""
133
+ retry_call(self.c.zrem, self.name, json.dumps(data))
134
+
135
+ def push(self, priority: int, data, vip=None) -> int:
136
+ """Add or update elements in the priority queue.
137
+
138
+ Existing elements will have their priority updated.
139
+
140
+ Returns:
141
+ Number of _NEW_ elements in the queue after the operation.
142
+ """
143
+ return retry_call(self.c.zadd, self.name, {json.dumps(data): -priority})
144
+
145
+ def pop(self, num=None):
146
+ if num is not None and num <= 0:
147
+ return []
148
+
149
+ if num:
150
+ return [decode(s[0]) for s in retry_call(self.c.zpopmin, self.name, num)]
151
+ else:
152
+ ret_val = retry_call(self.c.zpopmin, self.name, 1)
153
+ if ret_val:
154
+ return decode(ret_val[0][0])
155
+ return None
156
+
157
+ def unpush(self, num=None):
158
+ if num is not None and num <= 0:
159
+ return []
160
+
161
+ if num:
162
+ return [decode(s[0]) for s in retry_call(self.c.zpopmax, self.name, num)]
163
+ else:
164
+ ret_val = retry_call(self.c.zpopmax, self.name, 1)
165
+ if ret_val:
166
+ return decode(ret_val[0][0])
167
+ return None
168
+
169
+ def dequeue_range(self, lower_limit="", upper_limit="", skip=0, num=1):
170
+ """Dequeue a number of elements, within a specified range of scores.
171
+
172
+ Limits given are inclusive, can be made exclusive, see redis docs on how to format limits for that.
173
+
174
+ NOTE: lower/upper limit is negated+swapped in the lua script, no need to do it here
175
+
176
+ :param lower_limit: The score of all dequeued elements must be higher or equal to this.
177
+ :param upper_limit: The score of all dequeued elements must be lower or equal to this.
178
+ :param skip: In the range of available items to dequeue skip over this many.
179
+ :param num: Maximum number of elements to dequeue.
180
+ :return: list
181
+ """
182
+ results = retry_call(
183
+ self._deque_range,
184
+ keys=[self.name],
185
+ args=[lower_limit, upper_limit, skip, num],
186
+ )
187
+ return [decode(res) for res in results]
188
+
189
+
190
+ def select(*queues, **kw):
191
+ timeout = kw.get("timeout", 0)
192
+ if len(queues) < 1:
193
+ raise HowlerTypeError("At least one queue must be specified")
194
+ if any([not isinstance(q, PriorityQueue) for q in queues]):
195
+ raise HowlerTypeError("Only NamedQueues supported")
196
+
197
+ c = queues[0].c
198
+ response = retry_call(c.bzpopmin, [q.name for q in queues], timeout)
199
+
200
+ if not response:
201
+ return response
202
+
203
+ return response[0].decode("utf-8"), json.loads(response[1][SORTING_KEY_LEN:])
204
+
205
+
206
+ def length(*queues: PriorityQueue) -> list[int]:
207
+ """Utility function for batch reading queue lengths."""
208
+ if not queues:
209
+ return []
210
+ pipeline = queues[0].c.pipeline(transaction=False)
211
+
212
+ for que in queues:
213
+ pipeline.zcard(que.name)
214
+
215
+ return retry_call(pipeline.execute)