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/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("/<id>", methods=["DELETE"])
111
+ @view_api.route("/<view_id>", methods=["DELETE"])
122
112
  @api_login(required_priv=["W"])
123
- def delete_view(id: str, user: User, **kwargs):
113
+ def delete_view(view_id: str, user: User, **kwargs):
124
114
  """Delete a view
125
115
 
126
116
  Variables:
127
- id => The id of the view to delete
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(id)
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(id)
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("/<id>", methods=["PUT"])
150
+ @view_api.route("/<view_id>", methods=["PUT"])
161
151
  @api_login(required_priv=["R", "W"])
162
- def update_view(id: str, user: User, **kwargs):
152
+ def update_view(view_id: str, user: User, **kwargs):
163
153
  """Update a view
164
154
 
165
155
  Variables:
166
- id => The id of the view to modify
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()) - {"title", "query", "span", "sort", "settings"}:
189
- return bad_request(err="Only title, query, span and sort can be updated.")
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(id)
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("/<id>/favourite", methods=["POST"])
213
+ @view_api.route("/<view_id>/favourite", methods=["POST"])
224
214
  @api_login(required_priv=["R", "W"])
225
- def set_as_favourite(id: str, **kwargs):
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
- id => The id of the view to add as a favourite
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(id)
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", []) + [id]))
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("/<id>/favourite", methods=["DELETE"])
256
+ @view_api.route("/<view_id>/favourite", methods=["DELETE"])
267
257
  @api_login(required_priv=["R", "W"])
268
- def remove_as_favourite(id, **kwargs):
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
- current_user["favourite_views"] = list(filter(lambda f: f != id, current_user.get("favourite_views", [])))
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.borealis.enabled:
142
- from howler.api.v1.borealis import borealis_api
141
+ if config.core.clue.enabled:
142
+ from howler.api.v1.clue import clue_api
143
143
 
144
- logger.debug("Enabled Borealis Integration")
145
- app.register_blueprint(borealis_api)
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")
@@ -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)