howler-api 2.10.0.dev255__py3-none-any.whl → 2.13.0.dev344__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.
- howler/api/__init__.py +1 -1
- howler/api/v1/auth.py +1 -1
- howler/api/v1/{borealis.py → clue.py} +24 -26
- howler/api/v1/dossier.py +4 -28
- howler/api/v1/hit.py +11 -7
- howler/api/v1/search.py +160 -17
- howler/api/v1/user.py +2 -2
- howler/api/v1/utils/etag.py +43 -5
- howler/api/v1/view.py +26 -34
- howler/app.py +4 -4
- howler/cronjobs/view_cleanup.py +88 -0
- howler/datastore/README.md +0 -2
- howler/datastore/collection.py +109 -132
- howler/datastore/howler_store.py +0 -45
- howler/datastore/store.py +25 -6
- howler/odm/base.py +1 -1
- howler/odm/helper.py +9 -6
- howler/odm/models/config.py +168 -8
- howler/odm/models/howler_data.py +2 -1
- howler/odm/models/lead.py +1 -10
- howler/odm/models/pivot.py +2 -11
- howler/odm/random_data.py +1 -1
- howler/security/__init__.py +2 -2
- howler/services/analytic_service.py +31 -0
- howler/services/config_service.py +2 -2
- howler/services/dossier_service.py +140 -7
- howler/services/hit_service.py +317 -72
- howler/services/lucene_service.py +14 -7
- howler/services/overview_service.py +44 -0
- howler/services/template_service.py +45 -0
- howler/utils/lucene.py +22 -2
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/METADATA +5 -5
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/RECORD +35 -32
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/WHEEL +1 -1
- {howler_api-2.10.0.dev255.dist-info → howler_api-2.13.0.dev344.dist-info}/entry_points.txt +0 -0
howler/api/v1/view.py
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
from flask import request
|
|
2
2
|
from mergedeep.mergedeep import merge
|
|
3
3
|
|
|
4
|
-
from howler.api import
|
|
5
|
-
bad_request,
|
|
6
|
-
created,
|
|
7
|
-
forbidden,
|
|
8
|
-
make_subapi_blueprint,
|
|
9
|
-
no_content,
|
|
10
|
-
not_found,
|
|
11
|
-
ok,
|
|
12
|
-
)
|
|
4
|
+
from howler.api import bad_request, created, forbidden, make_subapi_blueprint, no_content, not_found, ok
|
|
13
5
|
from howler.common.exceptions import HowlerException
|
|
14
6
|
from howler.common.loader import datastore
|
|
15
7
|
from howler.common.logging import get_logger
|
|
@@ -46,9 +38,7 @@ def get_views(user: User, **kwargs):
|
|
|
46
38
|
try:
|
|
47
39
|
return ok(
|
|
48
40
|
datastore().view.search(
|
|
49
|
-
f"type:global OR owner:({user['uname']} OR none)",
|
|
50
|
-
as_obj=False,
|
|
51
|
-
rows=1000,
|
|
41
|
+
f"type:global OR owner:({user['uname']} OR none)", as_obj=False, rows=1000, sort="title asc"
|
|
52
42
|
)["items"]
|
|
53
43
|
)
|
|
54
44
|
except ValueError as e:
|
|
@@ -118,13 +108,13 @@ def create_view(**kwargs):
|
|
|
118
108
|
|
|
119
109
|
|
|
120
110
|
@generate_swagger_docs()
|
|
121
|
-
@view_api.route("/<
|
|
111
|
+
@view_api.route("/<view_id>", methods=["DELETE"])
|
|
122
112
|
@api_login(required_priv=["W"])
|
|
123
|
-
def delete_view(
|
|
113
|
+
def delete_view(view_id: str, user: User, **kwargs):
|
|
124
114
|
"""Delete a view
|
|
125
115
|
|
|
126
116
|
Variables:
|
|
127
|
-
|
|
117
|
+
view_id => The id of the view to delete
|
|
128
118
|
|
|
129
119
|
Optional Arguments:
|
|
130
120
|
None
|
|
@@ -139,7 +129,7 @@ def delete_view(id: str, user: User, **kwargs):
|
|
|
139
129
|
"""
|
|
140
130
|
storage = datastore()
|
|
141
131
|
|
|
142
|
-
existing_view: View = storage.view.get_if_exists(
|
|
132
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
143
133
|
if not existing_view:
|
|
144
134
|
return not_found(err="This view does not exist")
|
|
145
135
|
|
|
@@ -149,7 +139,7 @@ def delete_view(id: str, user: User, **kwargs):
|
|
|
149
139
|
if existing_view.type == "readonly":
|
|
150
140
|
return forbidden(err="You cannot delete built-in views.")
|
|
151
141
|
|
|
152
|
-
success = storage.view.delete(
|
|
142
|
+
success = storage.view.delete(view_id)
|
|
153
143
|
|
|
154
144
|
storage.view.commit()
|
|
155
145
|
|
|
@@ -157,13 +147,13 @@ def delete_view(id: str, user: User, **kwargs):
|
|
|
157
147
|
|
|
158
148
|
|
|
159
149
|
@generate_swagger_docs()
|
|
160
|
-
@view_api.route("/<
|
|
150
|
+
@view_api.route("/<view_id>", methods=["PUT"])
|
|
161
151
|
@api_login(required_priv=["R", "W"])
|
|
162
|
-
def update_view(
|
|
152
|
+
def update_view(view_id: str, user: User, **kwargs):
|
|
163
153
|
"""Update a view
|
|
164
154
|
|
|
165
155
|
Variables:
|
|
166
|
-
|
|
156
|
+
view_id => The view_id of the view to modify
|
|
167
157
|
|
|
168
158
|
Optional Arguments:
|
|
169
159
|
None
|
|
@@ -185,10 +175,10 @@ def update_view(id: str, user: User, **kwargs):
|
|
|
185
175
|
if not isinstance(new_data, dict):
|
|
186
176
|
return bad_request(err="Invalid data format")
|
|
187
177
|
|
|
188
|
-
if set(new_data.keys())
|
|
189
|
-
return bad_request(err="
|
|
178
|
+
if set(new_data.keys()) & {"view_id", "owner"}:
|
|
179
|
+
return bad_request(err="You cannot change the owner or id of a view.")
|
|
190
180
|
|
|
191
|
-
existing_view: View = storage.view.get_if_exists(
|
|
181
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
192
182
|
if not existing_view:
|
|
193
183
|
return not_found(err="This view does not exist")
|
|
194
184
|
|
|
@@ -220,13 +210,13 @@ def update_view(id: str, user: User, **kwargs):
|
|
|
220
210
|
|
|
221
211
|
|
|
222
212
|
@generate_swagger_docs()
|
|
223
|
-
@view_api.route("/<
|
|
213
|
+
@view_api.route("/<view_id>/favourite", methods=["POST"])
|
|
224
214
|
@api_login(required_priv=["R", "W"])
|
|
225
|
-
def set_as_favourite(
|
|
215
|
+
def set_as_favourite(view_id: str, **kwargs):
|
|
226
216
|
"""Add a view to a list of the user's favourites
|
|
227
217
|
|
|
228
218
|
Variables:
|
|
229
|
-
|
|
219
|
+
view_id => The id of the view to add as a favourite
|
|
230
220
|
|
|
231
221
|
Optional Arguments:
|
|
232
222
|
None
|
|
@@ -241,7 +231,7 @@ def set_as_favourite(id: str, **kwargs):
|
|
|
241
231
|
"""
|
|
242
232
|
storage = datastore()
|
|
243
233
|
|
|
244
|
-
existing_view: View = storage.view.get_if_exists(
|
|
234
|
+
existing_view: View = storage.view.get_if_exists(view_id)
|
|
245
235
|
if not existing_view:
|
|
246
236
|
return not_found(err="This view does not exist")
|
|
247
237
|
|
|
@@ -253,7 +243,7 @@ def set_as_favourite(id: str, **kwargs):
|
|
|
253
243
|
try:
|
|
254
244
|
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
255
245
|
|
|
256
|
-
current_user["favourite_views"] = list(set(current_user.get("favourite_views", []) + [
|
|
246
|
+
current_user["favourite_views"] = list(set(current_user.get("favourite_views", []) + [view_id]))
|
|
257
247
|
|
|
258
248
|
storage.user.save(current_user["uname"], current_user)
|
|
259
249
|
|
|
@@ -263,9 +253,9 @@ def set_as_favourite(id: str, **kwargs):
|
|
|
263
253
|
|
|
264
254
|
|
|
265
255
|
@generate_swagger_docs()
|
|
266
|
-
@view_api.route("/<
|
|
256
|
+
@view_api.route("/<view_id>/favourite", methods=["DELETE"])
|
|
267
257
|
@api_login(required_priv=["R", "W"])
|
|
268
|
-
def remove_as_favourite(
|
|
258
|
+
def remove_as_favourite(view_id: str, **kwargs):
|
|
269
259
|
"""Remove a view from a list of the user's favourites
|
|
270
260
|
|
|
271
261
|
Variables:
|
|
@@ -281,13 +271,15 @@ def remove_as_favourite(id, **kwargs):
|
|
|
281
271
|
"""
|
|
282
272
|
storage = datastore()
|
|
283
273
|
|
|
284
|
-
if not storage.view.exists(id):
|
|
285
|
-
return not_found(err="This view does not exist")
|
|
286
|
-
|
|
287
274
|
try:
|
|
288
275
|
current_user = storage.user.get_if_exists(kwargs["user"]["uname"])
|
|
289
276
|
|
|
290
|
-
|
|
277
|
+
current_favourites: list[str] = current_user.get("favourite_views", [])
|
|
278
|
+
|
|
279
|
+
if view_id not in current_favourites:
|
|
280
|
+
return not_found(err="View is not favourited.")
|
|
281
|
+
|
|
282
|
+
current_user["favourite_views"] = [favourite for favourite in current_favourites if favourite != view_id]
|
|
291
283
|
|
|
292
284
|
storage.user.save(current_user["uname"], current_user)
|
|
293
285
|
|
howler/app.py
CHANGED
|
@@ -138,11 +138,11 @@ if HWL_USE_REST_API or DEBUG:
|
|
|
138
138
|
logger.debug("Enabled Notebook Integration")
|
|
139
139
|
app.register_blueprint(notebook_api)
|
|
140
140
|
|
|
141
|
-
if config.core.
|
|
142
|
-
from howler.api.v1.
|
|
141
|
+
if config.core.clue.enabled:
|
|
142
|
+
from howler.api.v1.clue import clue_api
|
|
143
143
|
|
|
144
|
-
logger.debug("Enabled
|
|
145
|
-
app.register_blueprint(
|
|
144
|
+
logger.debug("Enabled Clue Integration")
|
|
145
|
+
app.register_blueprint(clue_api)
|
|
146
146
|
|
|
147
147
|
logger.info("Checking plugins for additional routes")
|
|
148
148
|
for plugin in get_plugins():
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, List
|
|
4
|
+
|
|
5
|
+
from apscheduler.schedulers.base import BaseScheduler
|
|
6
|
+
from apscheduler.triggers.cron import CronTrigger
|
|
7
|
+
from pytz import timezone
|
|
8
|
+
|
|
9
|
+
from howler.common.logging import get_logger
|
|
10
|
+
from howler.config import DEBUG, config
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__file__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def execute():
|
|
16
|
+
"""Delete any pinned views that no longer exist"""
|
|
17
|
+
from howler.common.loader import datastore
|
|
18
|
+
|
|
19
|
+
# Initialize datastore
|
|
20
|
+
ds = datastore()
|
|
21
|
+
# fetch the first result from user ds (needed to initialize total)
|
|
22
|
+
result = ds.user.search("*:*", rows=250, fl="*")
|
|
23
|
+
total_user_count = result["total"]
|
|
24
|
+
user_list: List[Any] = result["items"]
|
|
25
|
+
# Do the same thing for the views
|
|
26
|
+
result = ds.view.search("*:*", rows=250)
|
|
27
|
+
total_view_count = result["total"]
|
|
28
|
+
view_list: List[Any] = result["items"]
|
|
29
|
+
view_ids: List[str] = []
|
|
30
|
+
|
|
31
|
+
# Collect all views
|
|
32
|
+
while len(view_list) < total_view_count:
|
|
33
|
+
view_list.extend(ds.view.search("*:*", rows=250, offset=len(user_list)))
|
|
34
|
+
|
|
35
|
+
# Collect all users
|
|
36
|
+
while len(user_list) < total_user_count:
|
|
37
|
+
user_list.extend(ds.user.search("*:*", rows=250, offset=len(user_list)))
|
|
38
|
+
|
|
39
|
+
for view in view_list:
|
|
40
|
+
view_ids.append(view["view_id"])
|
|
41
|
+
|
|
42
|
+
# Iterate over each user to see if the dashboard contains invalid entries (deleted views)
|
|
43
|
+
for user in user_list:
|
|
44
|
+
valid_entries = []
|
|
45
|
+
# No views/analytics saved to the dashboard? Skip it
|
|
46
|
+
if user["dashboard"] == []:
|
|
47
|
+
continue
|
|
48
|
+
for dashboard_entry in user["dashboard"]:
|
|
49
|
+
if dashboard_entry["type"] != "view" or (
|
|
50
|
+
dashboard_entry["type"] == "view" and dashboard_entry["entry_id"] in view_ids
|
|
51
|
+
):
|
|
52
|
+
valid_entries.append(dashboard_entry)
|
|
53
|
+
# If the length of valid entries is less than the current dashboard, one or more pins are invalid
|
|
54
|
+
if len(valid_entries) < len(user["dashboard"]):
|
|
55
|
+
# set the user dashboard to valid entries
|
|
56
|
+
user["dashboard"] = valid_entries
|
|
57
|
+
# update the user
|
|
58
|
+
ds.user.save(user["uname"], user)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def setup_job(sched: BaseScheduler):
|
|
62
|
+
"""Initialize the view cleanup job"""
|
|
63
|
+
if not config.system.view_cleanup.enabled:
|
|
64
|
+
if not DEBUG or config.system.type == "production":
|
|
65
|
+
logger.warning("view cleanup cronjob disabled! This is not recommended for a production settings.")
|
|
66
|
+
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
logger.debug(f"Initializing view cleanup cronjob with cron {config.system.view_cleanup.crontab}")
|
|
70
|
+
|
|
71
|
+
if DEBUG:
|
|
72
|
+
_kwargs: dict[str, Any] = {"next_run_time": datetime.now()}
|
|
73
|
+
else:
|
|
74
|
+
_kwargs = {}
|
|
75
|
+
|
|
76
|
+
if sched.get_job("view_cleanup"):
|
|
77
|
+
logger.debug("view cleanup job already running!")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
sched.add_job(
|
|
81
|
+
id="view_cleanup",
|
|
82
|
+
func=execute,
|
|
83
|
+
trigger=CronTrigger.from_crontab(
|
|
84
|
+
config.system.view_cleanup.crontab, timezone=timezone(os.getenv("SCHEDULER_TZ", "America/Toronto"))
|
|
85
|
+
),
|
|
86
|
+
**_kwargs,
|
|
87
|
+
)
|
|
88
|
+
logger.debug("Initialization complete")
|
howler/datastore/README.md
CHANGED
|
@@ -83,8 +83,6 @@ class MyDatastore(object):
|
|
|
83
83
|
Once you've setup your own datastore object, you can start using the different functions that each collection offers. Here's a breakdown:
|
|
84
84
|
|
|
85
85
|
- `archive(query)`: Send all meatching documents to the archive of the collection
|
|
86
|
-
- `get_bulk_plan()`: Create a bulk plan to be executed later using the bulk function
|
|
87
|
-
- `bulk(bulk_plan)`: Execute the bulk plan
|
|
88
86
|
- `multiget(id_list)`: Get multiple documents for the id_list
|
|
89
87
|
- `exists(id)`: Check if a document matching this id exists
|
|
90
88
|
- `get(id)`: Get a document matching the id (retry twice if missing)
|