django-restit 4.2.173__py3-none-any.whl → 4.2.175__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.
@@ -0,0 +1,18 @@
1
+ # Generated by Django 4.2.18 on 2025-03-23 19:54
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('account', '0022_alter_memberdevice_modified'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='authsession',
15
+ name='method',
16
+ field=models.CharField(blank=True, db_index=True, default=None, max_length=127, null=True),
17
+ ),
18
+ ]
account/models/group.py CHANGED
@@ -118,6 +118,7 @@ class Group(models.Model, RestModel, MetaDataModel):
118
118
  "kind",
119
119
  ("metadata__timezone", "timezone"),
120
120
  ("metadata__eod", "end_of_day"),
121
+ ("parent__pk", "parent_id"),
121
122
  ("parent__name", "parent"),
122
123
  ("location.line1", "address_line1"),
123
124
  ("location.line2", "address_line2"),
@@ -171,7 +172,7 @@ class Group(models.Model, RestModel, MetaDataModel):
171
172
  if item:
172
173
  return item.thumbnail_url()
173
174
  return None
174
-
175
+
175
176
  @classmethod
176
177
  def on_rest_list_filter(cls, request, qset=None):
177
178
  if not request.member.hasPermission("view_all_groups"):
@@ -231,7 +232,7 @@ class Group(models.Model, RestModel, MetaDataModel):
231
232
  raise RestValidationError("cannot set self as parent", 1101)
232
233
  value = Group.objects.filter(pk=value).last()
233
234
  self.parent = value
234
-
235
+
235
236
  def set_child_of(self, value):
236
237
  # this is a helper to add this group to another group
237
238
  parent = Group.objects.filter(pk=value).last()
@@ -321,7 +322,7 @@ class Group(models.Model, RestModel, MetaDataModel):
321
322
  return True
322
323
  return False
323
324
 
324
- def notifyMembers(self, subject, message=None, template=None, context=None,
325
+ def notifyMembers(self, subject, message=None, template=None, context=None,
325
326
  email_only=False, sms_msg=None, perms=None, force=False,
326
327
  from_email=None, exclude_member=None):
327
328
  if perms is not None:
@@ -436,19 +437,19 @@ class Group(models.Model, RestModel, MetaDataModel):
436
437
  qset = self.memberships.filter(state__gte=-10)
437
438
  if perms:
438
439
  qset = qset.filter(
439
- Q(properties__category="permissions",
440
- properties__key__in=perms,
441
- properties__value__in=TRUE_VALUES) |
442
- Q(member__properties__category="permissions",
443
- member__properties__key__in=perms,
440
+ Q(properties__category="permissions",
441
+ properties__key__in=perms,
442
+ properties__value__in=TRUE_VALUES) |
443
+ Q(member__properties__category="permissions",
444
+ member__properties__key__in=perms,
444
445
  member__properties__value__in=TRUE_VALUES))
445
446
  if notify:
446
447
  qset = qset.filter(
447
- Q(properties__category="notify",
448
- properties__key__in=notify,
449
- properties__value__in=TRUE_VALUES) |
450
- Q(member__properties__category="notify",
451
- member__properties__key__in=notify,
448
+ Q(properties__category="notify",
449
+ properties__key__in=notify,
450
+ properties__value__in=TRUE_VALUES) |
451
+ Q(member__properties__category="notify",
452
+ member__properties__key__in=notify,
452
453
  member__properties__value__in=TRUE_VALUES))
453
454
 
454
455
  if role:
@@ -495,7 +496,7 @@ class Group(models.Model, RestModel, MetaDataModel):
495
496
  # enable again?
496
497
  ms.set_state(0)
497
498
  ms.save()
498
-
499
+
499
500
  if MEMBERSHIP_ROLES:
500
501
  perms = MEMBERSHIP_ROLES.get(role, [])
501
502
  for k in perms:
@@ -601,12 +602,8 @@ class Group(models.Model, RestModel, MetaDataModel):
601
602
  if credentials.kind == "member":
602
603
  if msg.pk is not None:
603
604
  return credentials.instance.isMemberOf(msg.pk)
604
- return False
605
+ return False
605
606
 
606
607
 
607
608
  class GroupMetaData(MetaDataBase):
608
609
  parent = models.ForeignKey(Group, related_name="properties", on_delete=models.CASCADE)
609
-
610
-
611
-
612
-
account/models/session.py CHANGED
@@ -42,6 +42,7 @@ class AuthSession(models.Model, RestModel):
42
42
  signature = models.CharField(max_length=127, db_index=True)
43
43
  ip = models.CharField(max_length=127, null=True, blank=True, db_index=True)
44
44
  buid = models.CharField(max_length=127, null=True, blank=True, default=None)
45
+ method = models.CharField(max_length=127, null=True, blank=True, db_index=True, default=None)
45
46
 
46
47
  member = models.ForeignKey("account.Member", null=True, blank=True, related_name="auth_sessions", on_delete=models.CASCADE)
47
48
  location = models.ForeignKey("location.GeoIP", related_name="auth_sessions", blank=True, null=True, default=None, on_delete=models.CASCADE)
@@ -72,8 +73,9 @@ class AuthSession(models.Model, RestModel):
72
73
  return f"{self.member.username} - {self.ip} - {self.os} - {self.browser}"
73
74
 
74
75
  @classmethod
75
- def NewSession(cls, request):
76
+ def NewSession(cls, request, method="jwt"):
76
77
  obj = cls(ip=request.ip, member=request.member, signature=request.signature)
78
+ obj.method = method
77
79
  obj.user_agent = request.META.get('HTTP_USER_AGENT', None)
78
80
  obj.last_activity = datetime.now()
79
81
  obj.buid = request.POST.get("__buid__", request.GET.get("__buid__", None))
@@ -92,4 +94,3 @@ class AuthSession(models.Model, RestModel):
92
94
  session.touch()
93
95
  return session
94
96
  return None
95
-
account/rpc/auth.py CHANGED
@@ -37,6 +37,7 @@ def member_login(request):
37
37
  @rd.never_cache
38
38
  def jwt_login(request):
39
39
  # poor mans JWT, carried over
40
+ auth_method = "basic"
40
41
  username = request.DATA.get('username', None)
41
42
  if not username:
42
43
  return rv.restPermissionDenied(request, "Password and/or Username is incorrect", error_code=422)
@@ -45,29 +46,31 @@ def jwt_login(request):
45
46
  return rv.restPermissionDenied(request, error=f"Password and/or Username is incorrect for {username}", error_code=422)
46
47
  auth_code = request.DATA.get(["auth_code", "code", "invite_token"], None)
47
48
  if username and auth_code:
49
+ # this is typically used for OAUTH (final)
48
50
  return member_login_uname_code(request, username, auth_code)
49
51
  password = request.DATA.get('password', None)
50
52
  member.canLogin(request) # throws exception if cannot login
51
53
  if member.requires_totp or member.has_totp:
54
+ auth_method = "basic+totp"
52
55
  resp = checkForTOTP(request, member)
53
56
  if resp is not None:
54
57
  return resp
55
58
  if not member.login(request=request, password=password, use_jwt=True):
56
59
  # we do not want permission denied catcher invoked as it is already handled in login method
57
60
  return rv.restStatus(request, False, error=f"Invalid Credentials {username}", error_code=401)
58
- return on_complete_jwt(request, member)
61
+ return on_complete_jwt(request, member, auth_method)
59
62
 
60
63
 
61
- def on_complete_jwt(request, member):
64
+ def on_complete_jwt(request, member, method="basic"):
62
65
  if member.security_token is None or member.security_token == JWT_KEY or member.force_single_session:
63
66
  member.refreshSecurityToken()
64
67
 
65
68
  member.log(
66
- "jwt_login", "jwt login succesful",
69
+ "jwt_login", "jwt login succesful",
67
70
  request, method="login", level=7)
68
71
 
69
72
  device_id = request.DATA.get(["device_id", "deviceID"])
70
-
73
+
71
74
  token = JWToken(
72
75
  user_id=member.pk,
73
76
  key=member.security_token,
@@ -80,13 +83,13 @@ def on_complete_jwt(request, member):
80
83
  request.signature = token.session_id
81
84
  request.device_id = device_id
82
85
  request.buid = request.DATA.get("__buid__", None)
83
- request.auth_session = am.AuthSession.NewSession(request)
86
+ request.auth_session = am.AuthSession.NewSession(request, method)
84
87
  if bool(device_id):
85
88
  am.MemberDevice.register(request, member, device_id)
86
-
89
+
87
90
  request.jwt_token = token.access_token # this tells the middleware to store in cookie
88
91
  return rv.restGet(
89
- request,
92
+ request,
90
93
  dict(
91
94
  access=token.access_token,
92
95
  refresh=token.refresh_token,
@@ -221,9 +224,9 @@ def member_login_uname_code(request, username, auth_code):
221
224
  member.login(request=request)
222
225
  member.save()
223
226
  member.log("code_login", "code login", request, method="login", level=8)
224
- if request.DATA.get("auth_method") == "basic":
227
+ if request.DATA.get("auth_method") == "basic" and ALLOW_BASIC_LOGIN:
225
228
  return rv.restGet(request, dict(id=member.pk, session_key=request.session.session_key))
226
- return on_complete_jwt(request, member)
229
+ return on_complete_jwt(request, member, "auth_code")
227
230
 
228
231
 
229
232
  @rd.url(r'^logout$')
@@ -296,7 +299,7 @@ def get_member_from_request(request):
296
299
  request.member = member
297
300
  member.log("login_blocked", "account is locked out", request, method="login", level=31)
298
301
  return member, rv.restPermissionDenied(request, error=f"{member.username} Account locked out", error_code=411)
299
- return member, None
302
+ return member, None
300
303
 
301
304
 
302
305
  @rd.urlPOST('forgot')
@@ -408,5 +411,3 @@ def totp_verify(request):
408
411
  return rv.restPermissionDenied(request, "invalid code")
409
412
  request.member.setProperty("totp_verified", 1)
410
413
  return rv.restStatus(request, True)
411
-
412
-
account/rpc/oauth.py CHANGED
@@ -20,37 +20,43 @@ def oauth_google_login(request):
20
20
  state = request.DATA.get("state")
21
21
  app_url = settings.DEFAULT_LOGIN_URL
22
22
 
23
- rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
23
+ # rh.log_print("google/login", request.DATA.toDict(), request.session.get("state"))
24
24
 
25
25
  if state:
26
+ # this is where we should pull out the passed in state and get the proper URL
26
27
  state = objict.fromJSON(rh.hexToString(state))
27
28
  rh.log_print("state", state)
28
29
  app_url = state.url
29
30
 
30
31
  if not code:
31
32
  params = urlencode({'error': error})
32
- return redirect(f"{app_url}?{params}")
33
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
34
+ return redirect(f"{app_url}{separator}{params}")
33
35
 
34
- redirect_uri = f"{settings.BASE_URL_SECURE}{REST_PREFIX}account/oauth/google/login"
36
+ redirect_uri = f"{request.scheme}://{request.get_host()}/{REST_PREFIX}account/oauth/google/login"
35
37
  auth_data = google.getAccessToken(code, redirect_uri)
36
38
  if auth_data is None or auth_data.access_token is None:
37
39
  params = urlencode({'error': "failed to get access token from google"})
38
- return redirect(f"{app_url}?{params}")
40
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
41
+ return redirect(f"{app_url}{separator}{params}")
39
42
 
40
43
  user_data = google.getUserInfo(auth_data.access_token)
41
44
  if user_data is None:
42
45
  params = urlencode({'error': "failed to get user data from google"})
43
- return redirect(f"{app_url}?{params}")
46
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
47
+ return redirect(f"{app_url}{separator}{params}")
44
48
 
45
49
  if not user_data.email:
46
50
  params = urlencode({'error': "no email with account"})
47
- return redirect(f"{app_url}?{params}")
51
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
52
+ return redirect(f"{app_url}{separator}{params}")
48
53
 
49
54
  # TODO allow new accounts?
50
55
  member = Member.objects.filter(email=user_data.email).last()
51
56
  if member is None:
52
57
  params = urlencode({'error': "user not found"})
53
- return redirect(f"{app_url}?{params}")
58
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
59
+ return redirect(f"{app_url}{separator}{params}")
54
60
 
55
61
  member.setProperties(auth_data, category="google_auth")
56
62
  member.setProperties(user_data, category="google")
@@ -69,13 +75,8 @@ def oauth_google_login(request):
69
75
  member.save()
70
76
  member.auditLog("user succesfully authenticated with google", "google_oauth", level=17)
71
77
 
72
- params = urlencode({'oauth_code': member.auth_code, "username":member.username})
78
+ params = urlencode({'oauth_code': member.auth_code, "username":member.username, "auth_method":"google_oauth"})
73
79
  rurl = None
74
- if "?" in app_url:
75
- if app_url[-1] == "?":
76
- rurl = f"{app_url}{params}"
77
- else:
78
- rurl = f"{app_url}&{params}"
79
- else:
80
- rurl = f"{app_url}?{params}"
80
+ separator = '&' if '?' in app_url and app_url[-1] != '?' else '?'
81
+ rurl = f"{app_url}{separator}{params}"
81
82
  return redirect(rurl)
account/rpc/passkeys.py CHANGED
@@ -51,4 +51,4 @@ def rest_on_passkeys_auth_complete(request):
51
51
  request.session.pop("fido2_state"),
52
52
  request.session.pop("fido2_rp_id"))
53
53
  # we now want to handle the JWT or basic login flow
54
- return on_complete_jwt(request, uk.member)
54
+ return on_complete_jwt(request, uk.member, "passkey")
auditlog/models.py CHANGED
@@ -55,10 +55,11 @@ class PersistentLog(models.Model, RestModel):
55
55
  class RestMeta:
56
56
  DEFAULT_SORT = "-when"
57
57
  CAN_SAVE = False
58
+ ESTIMATE_COUNTS = True
58
59
  GROUP_FIELD = None # do this so when we sort by group it is always exact
59
60
  QUERY_FIELDS = [
60
- "level", "component", "pkey", "action",
61
- "request_method", "request_path",
61
+ "level", "component", "pkey", "action",
62
+ "request_method", "request_path",
62
63
  "session__ip", "user",
63
64
  "tid", "group", "user__username"]
64
65
  SEARCH_FIELDS = ["user__username", "user__last_name", "message", "session__ip", "request_path"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-restit
3
- Version: 4.2.173
3
+ Version: 4.2.175
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -24,17 +24,18 @@ account/migrations/0019_group_location.py,sha256=EfMB_w4qWUGDqQeNc453PFZwpjpTeoA
24
24
  account/migrations/0020_cloudcredentials_cloudcredentialsmetadata.py,sha256=mHwxkyDfA4ueQOt34w5ndJB4XwNTDLv79CkKgzhlz-c,2250
25
25
  account/migrations/0021_alter_cloudcredentials_group.py,sha256=zoFYmE-hd3uRGX6DRO9k-osPwH0jFeTU7S-pjCOtakk,561
26
26
  account/migrations/0022_alter_memberdevice_modified.py,sha256=9eeKcdr9p6qFJ8ZxSnKSj1KxZjW8NZfM0YCMck6i0QQ,424
27
+ account/migrations/0023_authsession_method.py,sha256=5oIOXyj24rlrLFq7frij5VBaWk_ckh7hRPqq_jpO_b0,452
27
28
  account/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  account/models/__init__.py,sha256=cV_lMnT2vL_mjiYtT4hlcIHo52ocFbGSNVkOIHHLXZY,385
29
30
  account/models/device.py,sha256=8D-Sbv9PZWAnX6UVpp1lNJ03P24fknNnN1VOhqY7RVg,6306
30
31
  account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
31
- account/models/group.py,sha256=N9Ow7AtV4xN5zSE9E5-msthJy0G5-3DEr2MTmvFO1kU,22887
32
+ account/models/group.py,sha256=JVyMIakLskUuGXBJFyessw4LlD9Fl6AsHtpo1yZEYjk,22884
32
33
  account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
33
34
  account/models/member.py,sha256=qmLCOVbNTRr4L-E7BbOMtv4V64QN7K-0pXDgnuB-AbY,54722
34
35
  account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
35
36
  account/models/notify.py,sha256=YKYEXT56i98b7-ydLt5UuEVOqW7lipQMi-KuiPhcSwY,15627
36
37
  account/models/passkeys.py,sha256=lObapudvL--ABSTZTIELmYvHE3dPF0tO_KmuYk0ZJXc,1699
37
- account/models/session.py,sha256=ELkWjB_2KXQvPtRPrvuGJpJsqrxCQX_4J53SbqGz_2U,3737
38
+ account/models/session.py,sha256=5tpyRF1BHTPBL5gBIx8Eu55sWc6pTziqDpm7Zm5bdJI,3876
38
39
  account/models/settings.py,sha256=gOyRWBVd3BQpjfj_hJPtqX3H46ztyRAFxBrPbv11lQg,2137
39
40
  account/oauth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  account/oauth/google.py,sha256=q5M6Qhpfp9QslKRVYFZBvtG6kgXV6vYMrR5fp6Xdb9I,2078
@@ -42,13 +43,13 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
42
43
  account/passkeys/core.py,sha256=4aUBNCuF_kjOvE1zFapK1Pj28ap5slO71dRyfnWi0YU,4148
43
44
  account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
44
45
  account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
45
- account/rpc/auth.py,sha256=O-co_vZM9T4lsu3hidjKAeL2tTPWgbmoLsrjhmFZ7lA,17064
46
+ account/rpc/auth.py,sha256=0LUTb-dO6wk6rWitPVAXeR0IqpbX5Yw8tHM5U9QJibA,17234
46
47
  account/rpc/device.py,sha256=lU2BHNPreHV0dDTjAPc7Sc-5m2JP8SiWVqiKuBfV7Fo,2281
47
48
  account/rpc/group.py,sha256=hw7iczZ6W_IrRbx5ZDw6cZ5I_ztqxhtUFJD9WR91_4s,4948
48
49
  account/rpc/member.py,sha256=8XnJX-iri0Om4nc-V2_tDJzfCSzziKLw6dUx9egtEZE,2236
49
50
  account/rpc/notify.py,sha256=Q2YWejP36egeF060Hih5uX4Psv_B8NWlLLPi7iDYlIw,3344
50
- account/rpc/oauth.py,sha256=ISLVsR5HvKALANokaOFRvF4FTRxWtXPvVnZAYANKxpo,2864
51
- account/rpc/passkeys.py,sha256=5x28nYILJUMMSwfVuWYL66hfoGUXahMqOwiHhM4I3Do,1729
51
+ account/rpc/oauth.py,sha256=1dMvEpoMcvGXEdgDq2OBZcRMnXcE1jD18-itVrZwH9A,3333
52
+ account/rpc/passkeys.py,sha256=AKgF2xgaGJi8UCFgfSMBh7rUqrJgVL8-RfDDRoPWiDM,1740
52
53
  account/rpc/settings.py,sha256=EvPuwW63Gp_Va0ANIPAZ894tnS_JCctQ0FzqYRdKUNM,271
53
54
  account/settings.py,sha256=XEvZdcA6p_iUpDq9NmICK8rxzIQ8NViKfrpyuYgSV4o,53
54
55
  account/templates/email/base.html,sha256=GUuatccaZtO_hLLNZmMQQKew1Bjfz3e6Z7p3dM6BrWk,9669
@@ -69,7 +70,7 @@ auditlog/middleware.py,sha256=Q4bXg8rnm8y2fMnAsN6ha3Fz6TW8jIzLnvpu4H9SpWE,1537
69
70
  auditlog/migrations/0001_initial.py,sha256=X171gKQZIaTO9FGNG1yKTjGSZS0ZjZj5gvimF9-_kks,3309
70
71
  auditlog/migrations/0002_alter_persistentlog_session.py,sha256=DkkcIobbHdbniKg5bOlRmiF-Nc4hX55Y6KuQySrCcJ8,541
71
72
  auditlog/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
- auditlog/models.py,sha256=skDAiuzR4chC-WNIaH2nm_VVcbnDD6ZtUxBwhk7UY8U,16517
73
+ auditlog/models.py,sha256=kRoWpOout2DYW70jsZLHj7GJBssqbrR75fx0_-hrsaM,16546
73
74
  auditlog/periodic.py,sha256=AUhDeVsZtC47BJ-lklvYEegHoxAzj1RpIvRFSsM7g5E,363
74
75
  auditlog/rpc.py,sha256=gJgj3Wiar5pVsw8tuhy0jXLkqFkOr3Z-oI2DKelMRAQ,5592
75
76
  auditlog/tq.py,sha256=ROcCjQhCavOgW3-8yjLrboNTAYEC5Pu2UCoenom0Law,2478
@@ -114,9 +115,9 @@ incident/migrations/0015_rule_title_template_alter_incident_state.py,sha256=FPUD
114
115
  incident/migrations/0016_rule_notify_template.py,sha256=4WGdMxiELujLIy9bzHovHWbAORupodN1Ty3vsy3mLjg,425
115
116
  incident/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
117
  incident/models/__init__.py,sha256=NMphuhb0RTMf7Ov4QkNv7iv6_I8Wtr3xQ54yjX_a31M,209
117
- incident/models/event.py,sha256=FEzEKKQKLkdRrPCvv-3Um9E2UPQjyIJp314Zr2LHuiw,7976
118
- incident/models/incident.py,sha256=dSveWs0VawYePeFxwZT8ni85CS_4jxpGV8nXPVGXokk,22607
119
- incident/models/ossec.py,sha256=eUDRGawzuLWobKEVGKfdZisDnyjS_Hlxi0T_GCSLCCI,2252
118
+ incident/models/event.py,sha256=JEUdVUxqz2nRcfGC2GHo3baIg5JntK5cSL6_MesFLeA,7967
119
+ incident/models/incident.py,sha256=oxwLDJYGmk26zf7AD_e7nKcoJw3gXibjrX3DlByNAT8,22580
120
+ incident/models/ossec.py,sha256=g7cc2vYdYEB8zomohwqbo0ekyPt1v_qA67y35sBn2YY,2244
120
121
  incident/models/rules.py,sha256=PPp8oJDW1gop9i_21lhP50qgt_TrdWErp2mYqZCMfd4,7065
121
122
  incident/models/ticket.py,sha256=S3kqGQpYLE6Y4M9IKu_60sgW-f592xNr8uufqHnvDoU,2302
122
123
  incident/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -356,7 +357,7 @@ metrics/migrations/0003_metrics_expires.py,sha256=_g4oRv4NHW-4iCQx2s1SiF38LLyFf8
356
357
  metrics/migrations/0004_eodmetrics.py,sha256=Ky6ZVMZqa0F_SUp_QFWY7ZKBgVhy9CS4wZcsEhrkSgc,3271
357
358
  metrics/migrations/0005_alter_metrics_v1_alter_metrics_v10_alter_metrics_v11_and_more.py,sha256=pmwJfpPJ1RUX_CqM66l6vvV-nrAUPo_GIan0Pc9mQHs,2358
358
359
  metrics/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
359
- metrics/models.py,sha256=O29Cud4VAwIAWnmQymFti0-ibOEoGmTWGxWsbtzcZHQ,14162
360
+ metrics/models.py,sha256=v3SKizBcKVQ8eFIT18fPr6XveQ1cLJZqLKiitncz5-0,14175
360
361
  metrics/periodic.py,sha256=IayBLLat40D8FB-A3bYBW9lxm9-IzcugQunojThQ_OU,661
361
362
  metrics/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
362
363
  metrics/providers/aws.py,sha256=dIBGFE1Fvszy6rmVrn_Fm1zUDv345q4dBsg9Iit-XCc,8358
@@ -379,7 +380,7 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
379
380
  rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
380
381
  rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
381
382
  rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
382
- rest/__init__.py,sha256=X-ZjYMsIpHcsnYPqEyCUT_cVtex5VAn-DlgZF8JJFg0,122
383
+ rest/__init__.py,sha256=5nuojPskvcFtHA31BnuuT6Z7MtZov2YpOn2IHnzm9cw,122
383
384
  rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
384
385
  rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
385
386
  rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
@@ -412,12 +413,12 @@ rest/middleware/request.py,sha256=JchRNy5L-bGd-7h-KFYekGRvREe2eCkZXKOYqIkP2hI,41
412
413
  rest/middleware/session.py,sha256=zHSoQpIzRLmpqr_JvW406wzpvU3W3gDbm5JhtzLAMlE,10240
413
414
  rest/middleware/session_store.py,sha256=1nSdeXK8PyuYgGgIufqrS6j6QpIrQ7zbMNT0ol75e6U,1901
414
415
  rest/models/__init__.py,sha256=M8pvFDq-WCF-QcM58X7pMufYYe0aaQ3U0PwGe9TKbbY,130
415
- rest/models/base.py,sha256=eOvVDwHgKDdBJAnaK2xavGfkjkfpk5Oqy1KfKRjtu6o,72072
416
+ rest/models/base.py,sha256=oPj4m4-fnmjYUqd9dru17ulyh8jdFbXtWJt1CiuiBls,72633
416
417
  rest/models/cacher.py,sha256=eKz8TINVhWEqKhJGMsRkKZTtBUIv5rN3NHbZwOC56Uk,578
417
418
  rest/models/metadata.py,sha256=ni8-BRF07lv4CdPUWnUdfPTOClQAVEeRZvO-ic623HU,12904
418
419
  rest/net.py,sha256=LcB2QV6VNRtsSdmiQvYZgwQUDwOPMn_VBdRiZ6OpI-I,2974
419
420
  rest/regexes.yaml,sha256=VoGb4E-P_K9f82Yzcpltgzekpt9usRtwu9PYlo46nUw,149463
420
- rest/requestex.py,sha256=N56056vV3zWkAP-QrYsCu7SdpPzE5kLuPoxGIuZAKt0,16127
421
+ rest/requestex.py,sha256=hv0ss8n2wojoBD70cx1DkUj1Msuhd5Nr8bdaYQNIPC0,16123
421
422
  rest/rpc.py,sha256=WXZe5CLdYMeSXRH6wuzl-_riPPRnmtFNVJB9dfY1GSo,2965
422
423
  rest/search.py,sha256=QVjk2b5tZLgf1zM2MHvJTyRjwUbY5ZD7HXSTmSPXtvU,8362
423
424
  rest/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -429,8 +430,9 @@ rest/serializers/legacy.py,sha256=a5O-x2PqMKX8wYWrhCmdcivVbkPnru7UdyLbrhCaAdY,61
429
430
  rest/serializers/localizers.py,sha256=BegaCvTQVaruhWzvGHq3zWeVFmtBChatquRqAtkke10,410
430
431
  rest/serializers/model.py,sha256=08HJeqpmytjxvyiJFfsSRRG0uH-iK2mXCw6w0oMfWrI,8598
431
432
  rest/serializers/profiler.py,sha256=OxOimhEyvCAuzUBC9Q1dz2xaakjAqmSnekMATsjduXM,997
432
- rest/serializers/response.py,sha256=nUUFITnTKPDtqD5qtSRzYHFqhsG1TxO7thH81Bq-8Vk,7602
433
+ rest/serializers/response.py,sha256=fsGgS9mtZSceXzNjMcjEJplisn4bG8qQSSdRGrq5Spo,8657
433
434
  rest/serializers/util.py,sha256=-In89fpuVTd6_Ul8nwEUt3DjVKdpeoEyAxudlyB8K6Y,2734
435
+ rest/service.py,sha256=jl8obnMDEUzB8y3LROGPvmfKKoFU_SzOvywUQjoQZpg,4046
434
436
  rest/settings_helper.py,sha256=_Vn9nmL5_GPss9zIsXzacbTQkn99NbO42CqvOZC3ge4,1532
435
437
  rest/ssl_check.py,sha256=kH4Pk4upUEwKTAnBLR0DIKezNJHjkW3g2TdQAObEgW4,1419
436
438
  rest/static/lib/jquery.js,sha256=VAvG3sHdS5LqTT-5A_aeq_bZGa_Uj04xKxY8KM_w9EE,95786
@@ -445,7 +447,7 @@ rest/templates/rest_html.html,sha256=utOzvXEE6LiHFUvcAQUDOQ0yl3J_2F3-22r0K611eTQ
445
447
  rest/ua.py,sha256=0wwOtJPWCiCxUKzWKyMApRbSaiuAxCCk0Pe3OwzYBhE,185398
446
448
  rest/uberdict.py,sha256=ivDpzfchQqX8dM2_TtuyMW7NNO-j7zDmxkdKixQxvU4,17064
447
449
  rest/url_docs.py,sha256=O8O_CQso3fB-7o-huidIT1BRGv5R6lDW-yKHnsGyAPk,11881
448
- rest/urls.py,sha256=HkmRhZVpOpzJDt6fZ16SJ54WBq34E15nt3cJ8qg-e8I,1870
450
+ rest/urls.py,sha256=MGMTDU_SN5ZO3HeomFsE6szKnMzE_X9vzh_J0YL0KGA,1866
449
451
  rest/views.py,sha256=WUjHQMCzw2f4Te2X4Kh40ksfoVAAlrWC2pbt8nhcIYA,1115
450
452
  sessionlog/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
451
453
  sessionlog/README.md,sha256=vQEVJ_8u3Vv19VwPfscjCiHFu61ZSrEM-KIuBpUXhws,62
@@ -471,7 +473,7 @@ taskqueue/transports/http.py,sha256=AzliUnw_LuyO2zZZOoUAJGFcTV-Gxt1iE3hCVnIiyGQ,
471
473
  taskqueue/transports/s3.py,sha256=fMosL893u1iQdo6Y1djwb7KEoNo6TTsDPJl13OJdJP8,1913
472
474
  taskqueue/transports/sftp.py,sha256=jT1_krjTHA7DCAukD85aGYRCg9m0cEH9EWzOC-wJGdk,1891
473
475
  taskqueue/transports/sms.py,sha256=H1-LIGEMfbUNqJD9amRcsvKUSwtz9yBj1QNfB7EHjHE,142
474
- taskqueue/worker.py,sha256=91n_GuwB-keozkyrgYcDpFUybV6EC2Yirnv8owPUwrg,16250
476
+ taskqueue/worker.py,sha256=H7KfcIj-lNd8K5Wn7tlw4rXYPqVdkxv2B5X5kbEcIbk,16054
475
477
  telephony/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
476
478
  telephony/admin.py,sha256=iOdsBfFFbBisdqKSZ36bIrh_z5sU0Wx_PkaFi8wd1iA,243
477
479
  telephony/decorators.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -515,7 +517,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
515
517
  ws4redis/settings.py,sha256=KKq00EwoGnz1yLwCZr5Dfoq2izivmAdsNEEM4EhZwN4,1610
516
518
  ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
517
519
  ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
518
- django_restit-4.2.173.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
519
- django_restit-4.2.173.dist-info/METADATA,sha256=i08Bup9bGciei5ZmKtc-1LMW236KZYtfSWUByHv6tfo,7714
520
- django_restit-4.2.173.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
521
- django_restit-4.2.173.dist-info/RECORD,,
520
+ django_restit-4.2.175.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
521
+ django_restit-4.2.175.dist-info/METADATA,sha256=DuVZg0LP0bkuNNdfm9IlJXwcpPE-O4-V6qSZYRHICTk,7714
522
+ django_restit-4.2.175.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
523
+ django_restit-4.2.175.dist-info/RECORD,,
incident/models/event.py CHANGED
@@ -18,8 +18,8 @@ INCIDENT_EVENT_GRANULARITY = settings.get("INCIDENT_EVENT_GRANULARITY", "hourly"
18
18
  EVENT_TO_INCIDENT_LEVEL = settings.get("EVENT_TO_INCIDENT_LEVEL", 4)
19
19
  EVENT_DETAIL_TEMPLATES = settings.get("EVENT_DETAIL_TEMPLATES", None)
20
20
  EVENT_META_KEYWORDS = settings.get("EVENT_META_KEYWORDS", [
21
- "path", "ip", "reporter_ip", "code",
22
- "reason", "buid", "merchant", "tid",
21
+ "path", "ip", "reporter_ip", "code",
22
+ "reason", "buid", "merchant", "tid",
23
23
  "group", "http_user_agent", "user_agent",
24
24
  "app_url", "isp", "city", "state", "country",
25
25
  "username"
@@ -28,7 +28,7 @@ EVENT_META_KEYWORDS = settings.get("EVENT_META_KEYWORDS", [
28
28
  logger = log.getLogger("incident", filename="incident.log")
29
29
 
30
30
  """
31
- very generic
31
+ very generic
32
32
  external system can post an event
33
33
  {
34
34
  "description": "Critical Test Event",
@@ -77,15 +77,15 @@ class Event(JSONMetaData, rm.RestModel):
77
77
  # code = models.IntegerField(default=0, db_index=True)
78
78
 
79
79
  group = models.ForeignKey(
80
- "account.Group", on_delete=models.SET_NULL,
80
+ "account.Group", on_delete=models.SET_NULL,
81
81
  related_name="+", null=True, default=None)
82
-
82
+
83
83
  component = models.SlugField(max_length=250, null=True, blank=True, default=None)
84
84
  component_id = models.IntegerField(null=True, blank=True, default=None)
85
85
 
86
86
  # this allows us to bundle multiple events to an incident
87
87
  incident = models.ForeignKey(
88
- Incident, null=True, default=None,
88
+ Incident, null=True, default=None,
89
89
  related_name="events", on_delete=models.SET_NULL)
90
90
 
91
91
  def runRules(self):
@@ -170,14 +170,14 @@ class Event(JSONMetaData, rm.RestModel):
170
170
  # logger.info(f"ignore event {self.pk} {self.description}")
171
171
  return
172
172
 
173
- # always create an incident
173
+ # always create an incident
174
174
  if incident is None:
175
175
  incident = Incident(
176
176
  rule=hit_rule, priority=priority,
177
177
  reporter_ip=self.reporter_ip,
178
178
  category=self.category,
179
179
  group=self.group,
180
- component=self.component,
180
+ component=self.component,
181
181
  component_id=self.component_id,
182
182
  hostname=self.hostname)
183
183
  if self.group is None and hit_rule is not None:
@@ -211,4 +211,3 @@ class Event(JSONMetaData, rm.RestModel):
211
211
  incident.triggerAction()
212
212
  except Exception:
213
213
  logger.exception()
214
-
@@ -92,7 +92,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
92
92
 
93
93
  group = models.ForeignKey("account.Group", on_delete=models.SET_NULL, null=True, default=None)
94
94
  assigned_to = models.ForeignKey("account.Member", on_delete=models.SET_NULL, null=True, default=None)
95
-
95
+
96
96
  priority = models.IntegerField(default=0) # 1-10, 1 being the highest
97
97
  # 0=new, 1=opened, 2=paused, 3=ignore, 4=resolved, 5=pending
98
98
  state = models.IntegerField(default=0, choices=INCIDENT_STATES)
@@ -160,7 +160,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
160
160
 
161
161
  if event.details:
162
162
  self.setProperty("details", event.details)
163
-
163
+
164
164
  if event.metadata.geoip and event.metadata.geoip.city:
165
165
  self.setProperty("city", event.metadata.geoip.city)
166
166
  self.setProperty("state", event.metadata.geoip.state)
@@ -209,7 +209,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
209
209
  self.save()
210
210
  elif self.rule.action.startswith("firewall_block"):
211
211
  if settings.FIREWALL_GLOBAL_BLOCK:
212
- Task.Publish("incident", "firewall_block",
212
+ Task.Publish("incident", "firewall_block",
213
213
  dict(ip=self.reporter_ip),
214
214
  channel="tq_broadcast")
215
215
 
@@ -348,7 +348,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
348
348
  member.sendSMS(sms_msg)
349
349
  members = group.getMembers(perms=email_perms, as_member=True)
350
350
  for member in members:
351
- member.sendEmail(subject, body)
351
+ member.sendEmail(subject, body)
352
352
 
353
353
  def notifyWith(self, perm):
354
354
  # logger.info("notifyWith", perm)
@@ -407,7 +407,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
407
407
  if request != None and "DATA" in request and "note" in request.DATA:
408
408
  self.logHistory(kind="note", note=request.DATA.get("note"), request=request)
409
409
 
410
- def logHistory(self, kind="history", note=None, media=None,
410
+ def logHistory(self, kind="history", note=None, media=None,
411
411
  request=None, member=None, notify=True):
412
412
  if request is None:
413
413
  request = self.getActiveRequest()
@@ -493,7 +493,7 @@ class Incident(models.Model, rm.RestModel, rm.MetaDataModel):
493
493
  email_only=True,
494
494
  from_email=INCIDENT_EMAIL_FROM)
495
495
  else:
496
- # notitfy everyone but the sender
496
+ # notitfy everyone but the sender
497
497
  if history.by is None:
498
498
  members = Member.GetWithPermission(perm, ignore_disabled_email=True).exclude(pk=history.by.pk)
499
499
  if members.count() == 0:
incident/models/ossec.py CHANGED
@@ -46,8 +46,8 @@ class ServerOssecAlert(models.Model, rm.RestModel):
46
46
  level = models.IntegerField(default=0)
47
47
  title = models.CharField(max_length=200, blank=True, null=True, default=None)
48
48
  geoip = models.ForeignKey("location.GeoIP", blank=True, null=True, default=None, on_delete=models.DO_NOTHING)
49
-
49
+
50
50
  metadata = None
51
-
51
+
52
52
  def __str__(self):
53
53
  return f'{self.hostname}: {self.title}'
metrics/models.py CHANGED
@@ -43,14 +43,14 @@ def metric(
43
43
  elif granularity in ["minutes", "seconds"]:
44
44
  expires = datetime.now() + timedelta(days=7)
45
45
  m, created = Metrics.objects.get_or_create(
46
- uuid=key,
46
+ uuid=key,
47
47
  defaults=dict(
48
48
  granularity=granularity, slug=slug, expires=expires,
49
49
  group=group, start=utils.date_for_granulatiry(date, granularity)))
50
50
  m.updateMetrics(keys, data, created)
51
51
 
52
52
 
53
- def gauge(slug, keys, data, granularity="daily", group=None, date=None,
53
+ def gauge(slug, keys, data, granularity="daily", group=None, date=None,
54
54
  timezone=None, slug_append=None, max_granularity=None):
55
55
  # guage does not accumulate but just stores the data like a cache
56
56
  # if calledf on the same time period it will just update the current numbers
@@ -72,7 +72,7 @@ def gauge(slug, keys, data, granularity="daily", group=None, date=None,
72
72
  elif gran == "daily":
73
73
  expires = datetime.now() + timedelta(days=METRICS_EXPIRE_DAILY)
74
74
  m, created = Metrics.objects.get_or_create(
75
- uuid=key,
75
+ uuid=key,
76
76
  defaults=dict(
77
77
  granularity=gran, slug=slug, expires=expires,
78
78
  group=group, start=utils.date_for_granulatiry(date, gran)))
@@ -99,7 +99,7 @@ def get_qset(slug, granularity, start=None, end=None,
99
99
  start = group.getLocalTime(start)
100
100
  elif settings.METRICS_TIMEZONE:
101
101
  start = date_util.convertToLocalTime(settings.METRICS_TIMEZONE, start)
102
-
102
+
103
103
  start = utils.date_for_granulatiry(start, granularity)
104
104
  if end is None:
105
105
  if granularity == "hourly":
@@ -133,7 +133,7 @@ def get_totals(slug, keys, granularity, start=None, end=None, group=None):
133
133
  start = group.getLocalTime(start)
134
134
  elif settings.METRICS_TIMEZONE:
135
135
  start = date_util.convertToLocalTime(settings.METRICS_TIMEZONE, start)
136
-
136
+
137
137
  start = utils.date_for_granulatiry(start, granularity)
138
138
  if end is None:
139
139
  end = start + timedelta(minutes=5)
@@ -141,7 +141,7 @@ def get_totals(slug, keys, granularity, start=None, end=None, group=None):
141
141
  end = group.getLocalTime(end)
142
142
  elif settings.METRICS_TIMEZONE:
143
143
  end = date_util.convertToLocalTime(settings.METRICS_TIMEZONE, end)
144
-
144
+
145
145
  qset = Metrics.objects.filter(
146
146
  slug=slug, granularity=granularity,
147
147
  group=group, start__gte=start, start__lte=end)
@@ -162,7 +162,7 @@ def get_metric(slug, granularity, start, group=None):
162
162
  start = group.getLocalTime(start)
163
163
  elif settings.METRICS_TIMEZONE:
164
164
  start = date_util.convertToLocalTime(settings.METRICS_TIMEZONE, start)
165
-
165
+
166
166
  start = utils.date_for_granulatiry(start, granularity)
167
167
  qset = Metrics.objects.filter(
168
168
  slug=slug, granularity=granularity, start__gte=start, start__lte=start, group=group)
@@ -226,7 +226,7 @@ def get_metrics(slug, granularity, start, end=None, group=None, samples=None):
226
226
  for k in keys:
227
227
  data[k].append(0)
228
228
  continue
229
- for k in keys:
229
+ for k in keys:
230
230
  data[k].append(result["values"].get(k, 0))
231
231
  period_values.remove(result)
232
232
  return objict(periods=periods, data=data)
@@ -241,7 +241,7 @@ def get_chart_periods(slug, granularity, start, end=None, group=None):
241
241
  periods.append(period)
242
242
  if granularity != "weekly":
243
243
  periods.reverse()
244
- return periods
244
+ return periods
245
245
 
246
246
 
247
247
  def get_adjusted_date_range(granularity, start, end, group=None, samples=None):
@@ -282,6 +282,7 @@ def generate_uuid(slug, group, slug_append=None):
282
282
  class Metrics(models.Model, rm.RestModel):
283
283
  class RestMeta:
284
284
  QUERY_FIELDS = ["group__kind", "all_fields"]
285
+ ESTIMATE_COUNTS = True
285
286
  GRAPHS = {
286
287
  "detailed": {
287
288
  "fields": [
@@ -310,7 +311,7 @@ class Metrics(models.Model, rm.RestModel):
310
311
  # allow to group metrics by a group
311
312
  group = models.ForeignKey("account.Group", related_name="+", on_delete=models.CASCADE, null=True, default=None)
312
313
 
313
- # now we create a set of k/v
314
+ # now we create a set of k/v
314
315
  k1 = models.SlugField(max_length=64, null=True, default=None)
315
316
  v1 = models.BigIntegerField(default=0)
316
317
 
rest/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .uberdict import UberDict # noqa: F401
2
2
  from .settings_helper import settings # noqa: F401
3
3
 
4
- __version__ = "4.2.173"
4
+ __version__ = "4.2.175"
rest/models/base.py CHANGED
@@ -7,6 +7,7 @@ import importlib
7
7
  from django.db import models as dm
8
8
  from django.db.transaction import atomic
9
9
  from django.apps import apps
10
+ from django.db import connection
10
11
 
11
12
  from rest import helpers as rh
12
13
  from rest import errors as re
@@ -876,7 +877,7 @@ class RestModel(object):
876
877
  if not graph and request is not None:
877
878
  graph = request.DATA.get("graph", "default")
878
879
  return GRAPH_HELPERS.restList(
879
- request, qset, sort=sort, totals=totals,
880
+ request, qset, sort=sort, totals=totals,
880
881
  return_httpresponse=return_httpresponse,
881
882
  response_params=dict(graph=graph),
882
883
  **cls.getGraph(graph))
@@ -896,6 +897,17 @@ class RestModel(object):
896
897
  request = cls.getActiveRequest()
897
898
  return cls.restList(request, qset, graph, totals, False)
898
899
 
900
+ @classmethod
901
+ def restListEstimatedCount(cls, request, qset):
902
+ # TODO attempt to make this work with the qset,
903
+ # right now it gets an estimated count of the entire table
904
+ if getattr(cls.RestMeta, "ESTIMATE_COUNTS", False):
905
+ if connection.vendor == 'postgresql':
906
+ with connection.cursor() as cursor:
907
+ cursor.execute(f"SELECT reltuples::bigint FROM pg_class WHERE relname = '{cls._meta.db_table}'")
908
+ return cursor.fetchone()[0]
909
+ return qset.count()
910
+
899
911
  def restStatus(self, request, status, **kwargs):
900
912
  RestModel._setupGraphHelpers()
901
913
  return GRAPH_HELPERS.restStatus(request, status, **kwargs)
@@ -959,7 +971,7 @@ class RestModel(object):
959
971
  has_perms = True
960
972
  if perms is not None:
961
973
  has_perms = cls.on_rest_list_has_perms(request, perms, qset)
962
-
974
+
963
975
  if request.group:
964
976
  qset = cls.on_rest_filter_children(request, qset)
965
977
  if not has_perms:
@@ -1040,7 +1052,7 @@ class RestModel(object):
1040
1052
  elif dr_offset > 0:
1041
1053
  dr_start = dr_start + timedelta(minutes=dr_offset)
1042
1054
  dr_end = dr_end + timedelta(minutes=dr_offset)
1043
-
1055
+
1044
1056
  rh.debug("tr_end changing", str(dr_start), str(dr_end))
1045
1057
  dr_field = request.DATA.get("dr_field", getattr(cls.RestMeta, "DATE_RANGE_FIELD", "created"))
1046
1058
  q = dict()
@@ -1145,7 +1157,7 @@ class RestModel(object):
1145
1157
  cls._boundRest()
1146
1158
  if format.startswith("json"):
1147
1159
  return GRAPH_HELPERS.views.restJSON(
1148
- request, qset, fields,
1160
+ request, qset, fields,
1149
1161
  name, format_size, localize=localize)
1150
1162
  elif format.startswith("csv"):
1151
1163
  return GRAPH_HELPERS.views.restCSV(
@@ -1159,7 +1171,7 @@ class RestModel(object):
1159
1171
  return GRAPH_HELPERS.views.restFlat(
1160
1172
  request, qset, fields,
1161
1173
  name, format_size, localize=localize)
1162
-
1174
+
1163
1175
  @classmethod
1164
1176
  def on_rest_list_summary(cls, request, qset):
1165
1177
  cls._boundRest()
@@ -1589,7 +1601,7 @@ class RestModel(object):
1589
1601
  "group": "group__kind"
1590
1602
  }
1591
1603
 
1592
- QUERY_FIELDS can be used to restrict what fields can be queried
1604
+ QUERY_FIELDS can be used to restrict what fields can be queried
1593
1605
  and add special fields:
1594
1606
 
1595
1607
  ?group__kind=iso
@@ -1600,7 +1612,7 @@ class RestModel(object):
1600
1612
  or
1601
1613
  <field_name>__<operator>=value
1602
1614
 
1603
- You cannot do <field_name>__<sub_field>__<operator> unless
1615
+ You cannot do <field_name>__<sub_field>__<operator> unless
1604
1616
  <field_name>__<sub_field> is
1605
1617
 
1606
1618
  allowed <operator>s
rest/requestex.py CHANGED
@@ -18,12 +18,12 @@ SAFE_ASCII = set(string.printable)
18
18
  def createFakeRequest(method="GET", **kwargs):
19
19
  member = kwargs.get("member", objict(is_authenticated=True, is_staff=True, is_superuser=True))
20
20
  return objict(
21
- META={},
21
+ META={},
22
22
  user=member,
23
23
  member=member,
24
24
  group=kwargs.get("group", None),
25
- method=method,
26
- _started=0,
25
+ method=method,
26
+ _started=0,
27
27
  DATA=RequestData(data=kwargs))
28
28
 
29
29
 
@@ -284,7 +284,6 @@ class RequestData(object):
284
284
  if value == '':
285
285
  if field_type in [int, float]:
286
286
  return field_type(0)
287
-
288
287
  if field_type in [int, str, float, str]:
289
288
  value = field_type(value)
290
289
  elif field_type is datetime:
@@ -1,6 +1,8 @@
1
1
  from django.http import HttpResponse
2
2
  from django.shortcuts import render
3
3
  from django.db.models.query import QuerySet
4
+ from django.core.cache import cache
5
+ import hashlib
4
6
 
5
7
  from rest import settings
6
8
  from version import VERSION
@@ -13,6 +15,27 @@ from . import csv
13
15
  from . import excel
14
16
  # from . import profiler
15
17
  STATUS_ON_PERM_DENIED = settings.get("STATUS_ON_PERM_DENIED", 403)
18
+ REST_LIST_CACHE_COUNT = settings.get("REST_LIST_CACHE_COUNT", False)
19
+
20
+
21
+ def get_query_hash(queryset):
22
+ """Generate a unique hash based on the queryset's SQL query and params."""
23
+ sql_query = str(queryset.query)
24
+ return hashlib.sha256(sql_query.encode()).hexdigest()
25
+
26
+
27
+ def get_cached_count(queryset, hash=None, timeout=1800):
28
+ """
29
+ Retrieves the cached count for a queryset or calculates & caches it.
30
+ """
31
+ if hash is None:
32
+ hash = get_query_hash(queryset)
33
+ cache_key = f"rest_hash_{hash}"
34
+ count = cache.get(cache_key)
35
+ if count is None:
36
+ count = queryset.count()
37
+ cache.set(cache_key, count, timeout=timeout)
38
+ return count
16
39
 
17
40
 
18
41
  def restStatus(request, status, data={}, **kwargs):
@@ -41,13 +64,20 @@ def restGet(request, obj, fields=None, extra=[], exclude=[], recurse_into=[], **
41
64
  data["elapsed"] = get_request_elapsed(request)
42
65
  return restResult(request, data)
43
66
 
67
+
44
68
  # @profiler.timeit
45
69
  def restList(request, qset, size=25, start=0, sort=None, fields=None, extra=[], exclude=[], recurse_into=[], **kwargs):
46
- count = 0
47
- if isinstance(qset, QuerySet):
48
- count = qset.count()
49
- elif isinstance(qset, list):
50
- count = len(qset)
70
+ count = kwargs.get('count', 0)
71
+ hash = None
72
+ if count == 0:
73
+ if isinstance(qset, QuerySet):
74
+ if REST_LIST_CACHE_COUNT and not request.DATA.get("no_cache", False):
75
+ hash = get_query_hash(qset)
76
+ count = get_cached_count(qset, hash)
77
+ else:
78
+ count = qset.count()
79
+ elif isinstance(qset, list):
80
+ count = len(qset)
51
81
  start = request.DATA.get("start", start, field_type=int)
52
82
  size = request.DATA.get("size", size, field_type=int)
53
83
  sort = request.DATA.get("sort", sort)
@@ -56,6 +86,8 @@ def restList(request, qset, size=25, start=0, sort=None, fields=None, extra=[],
56
86
  data["datetime"] = int(time.time())
57
87
  data["elapsed"] = get_request_elapsed(request)
58
88
  data["count"] = count
89
+ if hash is not None:
90
+ data["hash"] = hash
59
91
  if "response_params" in kwargs:
60
92
  for k, v in kwargs.get("response_params").items():
61
93
  data[k] = v
rest/service.py ADDED
@@ -0,0 +1,135 @@
1
+ import os
2
+ import signal
3
+ import time
4
+ import argparse
5
+ import logging
6
+ import daemon
7
+ from daemon import pidfile
8
+ from watchdog.observers import Observer
9
+ from watchdog.events import FileSystemEventHandler
10
+
11
+
12
+ class Service(FileSystemEventHandler):
13
+ def __init__(self, name, pid_file, watch_file=None, require_user=None, logger=None):
14
+ self.name = name
15
+ self.pid_file = pid_file
16
+ self.watch_file = os.path.realpath(watch_file) if watch_file else None
17
+ self.require_user = require_user
18
+ self.observer = None
19
+
20
+ def log_info(self, *args):
21
+ if self.logger:
22
+ self.log_info(*args)
23
+
24
+ def start(self):
25
+ if self.is_running():
26
+ self.log_info("Service is already running.")
27
+ return
28
+
29
+ self.log_info(f"Starting {self.name}...")
30
+
31
+ with daemon.DaemonContext(
32
+ pidfile=pidfile.TimeoutPIDLockFile(self.pid_file),
33
+ stdout=open('/tmp/service.log', 'a+'),
34
+ stderr=open('/tmp/service_error.log', 'a+'),
35
+ signal_map={
36
+ signal.SIGTERM: self.stop,
37
+ signal.SIGHUP: self.restart
38
+ }
39
+ ):
40
+ self.run()
41
+
42
+ def stop(self, *args):
43
+ pid = self.get_pid()
44
+ if not pid:
45
+ self.log_info("Service is not running.")
46
+ return
47
+
48
+ self.log_info(f"Stopping {self.name} (PID: {pid})...")
49
+ try:
50
+ os.kill(pid, signal.SIGTERM)
51
+ time.sleep(1)
52
+ if self.is_running():
53
+ os.kill(pid, signal.SIGKILL)
54
+ if os.path.exists(self.pid_file):
55
+ os.remove(self.pid_file)
56
+ self.log_info("Service stopped.")
57
+ except OSError:
58
+ self.logger.error(f"Failed to stop process {pid}")
59
+
60
+ def restart(self, *args):
61
+ self.log_info("Restarting service...")
62
+ self.stop()
63
+ time.sleep(1)
64
+ self.start()
65
+
66
+ def status(self):
67
+ if self.is_running():
68
+ print(f"{self.name} is running (PID: {self.get_pid()})")
69
+ else:
70
+ print(f"{self.name} is not running")
71
+
72
+ def is_running(self):
73
+ """Check if the process is running using os.kill(pid, 0)"""
74
+ pid = self.get_pid()
75
+ if pid:
76
+ try:
77
+ os.kill(pid, 0) # Does not kill, just checks if process exists
78
+ return True
79
+ except OSError:
80
+ return False
81
+ return False
82
+
83
+ def get_pid(self):
84
+ if os.path.exists(self.pid_file):
85
+ try:
86
+ with open(self.pid_file, "r") as f:
87
+ pid = int(f.read().strip())
88
+ return pid
89
+ except ValueError:
90
+ return None
91
+ return None
92
+
93
+ def run(self):
94
+ """Main daemon loop"""
95
+ self.log_info(f"{self.name} is running in background.")
96
+ if self.watch_file:
97
+ self.start_watcher()
98
+ self.on_run()
99
+
100
+ def on_run(self):
101
+ while True:
102
+ time.sleep(5)
103
+
104
+ def start_watcher(self):
105
+ self.log_info(f"Watching file: {self.watch_file}")
106
+ self.observer = Observer()
107
+ self.observer.schedule(self, path=os.path.dirname(self.watch_file), recursive=False)
108
+ self.observer.start()
109
+
110
+ def on_modified(self, event):
111
+ if self.watch_file and event.src_path == self.watch_file:
112
+ self.log_info("Config file changed, restarting service...")
113
+ self.restart()
114
+
115
+
116
+ def main():
117
+ parser = argparse.ArgumentParser(description="Manage the service daemon")
118
+ parser.add_argument("command", choices=["start", "stop", "restart", "status", "run"])
119
+ args = parser.parse_args()
120
+
121
+ service = Service(name="MyDaemon", pid_file="/tmp/mydaemon.pid")
122
+
123
+ if args.command == "start":
124
+ service.start()
125
+ elif args.command == "stop":
126
+ service.stop()
127
+ elif args.command == "restart":
128
+ service.restart()
129
+ elif args.command == "status":
130
+ service.status()
131
+ elif args.command == "run":
132
+ service.run()
133
+
134
+ if __name__ == "__main__":
135
+ main()
rest/urls.py CHANGED
@@ -63,7 +63,3 @@ for app in settings.INSTALLED_APPS:
63
63
  sub_modules = getattr(module, "RPC_MODULES")
64
64
  for m in sub_modules:
65
65
  load_app(m, module)
66
-
67
-
68
-
69
-
taskqueue/worker.py CHANGED
@@ -46,23 +46,23 @@ class WorkManager(object):
46
46
  self.lock = threading.RLock()
47
47
  if not self.logger:
48
48
  self.logger = getLogger("root", filename="tq_worker.log")
49
- self.logger.info("starting manager, workers: {}".format(self.worker_count))
50
- self.logger.info("handling: {}".format(self.subscribe_to))
49
+ self.logger.info(f"starting manager, workers: {self.worker_count}")
50
+ self.logger.info(f"handling: {self.subscribe_to}")
51
51
  if USE_THREADS:
52
52
  self._pool = futures.ThreadPoolExecutor(max_workers=self.worker_count)
53
53
  else:
54
54
  self._pool = futures.ProcessPoolExecutor(max_workers=self.worker_count)
55
55
 
56
56
  def updateCounts(self):
57
- self.logger.info("running: {} --- pending: {}".format(self._running_count, self._pending_count))
57
+ self.logger.info(f"running: {self._running_count} --- pending: {self._pending_count}")
58
58
 
59
59
  def addTask(self, task):
60
60
  if task.is_stale:
61
- self.logger.warning("task({}) is now stale".format(task.id))
61
+ self.logger.warning(f"task({task.id}) is now stale")
62
62
  task.failed("stale")
63
63
  return
64
64
  if task.id in self._scheduled_tasks:
65
- self.logger.error("task({}) is already scheduled".format(task.id))
65
+ self.logger.error(f"task({task.id}) is already scheduled")
66
66
  return
67
67
  task.manager = self
68
68
  with self.lock:
@@ -76,7 +76,7 @@ class WorkManager(object):
76
76
  # self.logger.info("processing event", event)
77
77
  if event.type == "subscribe":
78
78
  # confirmation we subscribed
79
- self.logger.info("succesfully subscribed to: {}".format(event.channel))
79
+ self.logger.info(f"succesfully subscribed to: {event.channel}")
80
80
  return
81
81
 
82
82
  self.logger.info(f"new_event@{event.channel}")
@@ -118,7 +118,7 @@ class WorkManager(object):
118
118
  cached_task = self._scheduled_tasks.get(task.id, None)
119
119
  if not cached_task:
120
120
  # task is not scheduled
121
- self.logger.warning("canceling non scheduled task({})".format(task.id))
121
+ self.logger.warning(f"canceling non scheduled task({task.id})")
122
122
  task.state = -2
123
123
  task.reason = reason
124
124
  task.save()
@@ -130,11 +130,11 @@ class WorkManager(object):
130
130
  task = cached_task
131
131
  if task.future.running():
132
132
  # right now we don't support canceling a running task but we will try!
133
- self.logger.warning("attempting to stop running task({})".format(task.id))
133
+ self.logger.warning(f"attempting to stop running task({task.id})")
134
134
  if self.killWorker(task._thread_id):
135
135
  time.sleep(2.0)
136
136
  if task.future.done():
137
- self.logger.info("succesfully killed task({}@{})".format(task.id, task._thread_id))
137
+ self.logger.info(f"succesfully killed task({task.id}@{task._thread_id})")
138
138
  task.state = -2
139
139
  task.reason = reason
140
140
  task.save()
@@ -144,14 +144,14 @@ class WorkManager(object):
144
144
  self.logger.warning("failed to kill worker")
145
145
  else:
146
146
  if task.future.cancel():
147
- self.logger.info("succesfully canceled task({})".format(task.id))
147
+ self.logger.info(f"succesfully canceled task({task.id})")
148
148
  task.state = -2
149
149
  task.reason = reason
150
150
  task.save()
151
151
  self.removeTask(task)
152
152
  return
153
153
  else:
154
- self.logger.error("failed to cancel task({})".format(task.id))
154
+ self.logger.error(f"failed to cancel task({task.id})")
155
155
 
156
156
  def killWorker(self, thread_id):
157
157
  import ctypes
@@ -175,16 +175,16 @@ class WorkManager(object):
175
175
  for task in tasks:
176
176
  if task.channel in self.subscribe_to:
177
177
  if task.cancel_requested:
178
- self.logger.info("task has cancel request {}".format(task.id))
178
+ self.logger.info(f"task has cancel request {task.id}")
179
179
  task.state = -2
180
180
  if not task.reason:
181
181
  task.reason = "task canceled"
182
182
  task.save()
183
183
  continue
184
- self.logger.debug("resubmitting job {}".format(task.id))
184
+ self.logger.debug(f"resubmitting job {task.id}")
185
185
  self.addTask(task)
186
186
  else:
187
- self.logger.warning("ignore job {}:{}".format(task.id, task.channel))
187
+ self.logger.warning(f"ignore job {task.id}:{task.channel}")
188
188
 
189
189
  def _on_webrequest(self, task):
190
190
  if http.REQUEST(task):
@@ -254,7 +254,7 @@ class WorkManager(object):
254
254
  def on_run_task(self, task):
255
255
  """ Handles execution of a task with structured error handling and refactored logic. """
256
256
 
257
- self.logger.info("running task({})".format(task.id))
257
+ self.logger.info(f"running task({task.id})")
258
258
 
259
259
  # Start task and handle cancel/stale conditions early
260
260
  if not self._initialize_task(task):
@@ -275,14 +275,14 @@ class WorkManager(object):
275
275
  self.on_task_started(task)
276
276
  task.refresh_from_db()
277
277
  task._thread_id = threading.current_thread().ident
278
- self.logger.debug("running on thread:{}".format(task._thread_id))
278
+ self.logger.debug(f"running on thread:{task._thread_id}")
279
279
 
280
280
  if task.state not in [0, 1, 2, 10] or task.cancel_requested:
281
- self.logger.info("task({}) was canceled?".format(task.id))
281
+ self.logger.info(f"task({task.id}) was canceled?")
282
282
  return self._end_task(task)
283
283
 
284
284
  if task.is_stale:
285
- self.logger.warning("task({}) is now stale".format(task.id))
285
+ self.logger.warning(f"task({task.id}) is now stale")
286
286
  task.failed("stale")
287
287
  return self._end_task(task)
288
288
 
@@ -324,13 +324,13 @@ class WorkManager(object):
324
324
  except Exception as err:
325
325
  self._handle_task_exception(task, err)
326
326
  except SystemExit:
327
- self.logger.error("task({}) was killed".format(task.id))
327
+ self.logger.error(f"task({task.id}) was killed")
328
328
  finally:
329
329
  self._end_task(task)
330
330
 
331
331
  def _handle_task_exception(self, task, err):
332
332
  """ Handles exceptions during task execution. """
333
- self.logger.exception("task({}) had exception: {}".format(task.id, err))
333
+ self.logger.exception(f"task({task.id}) had exception: {err}")
334
334
  task.log_exception(err)
335
335
 
336
336
  if "connection already closed" in str(err).lower():
@@ -342,7 +342,7 @@ class WorkManager(object):
342
342
  def _end_task(self, task):
343
343
  """ Ensures proper cleanup and logging at the end of the task. """
344
344
  self.on_task_ended(task)
345
- self.logger.info("task({}) finished with state {}".format(task.id, task.state))
345
+ self.logger.info(f"task({task.id}) finished with state {task.state}")
346
346
 
347
347
  def run_forever(self):
348
348
  self.logger.info("starting work manager...")
@@ -379,7 +379,7 @@ class WorkManager(object):
379
379
  if self.host_channel not in self.subscribe_to:
380
380
  self.subscribe_to.append(self.host_channel)
381
381
  for key in self.subscribe_to:
382
- self.logger.info("subscribing to: {}".format(key))
382
+ self.logger.info(f"subscribing to: {key}")
383
383
  self.pubsub.subscribe(key)
384
384
  self.pubsub.subscribe("tq_cancel")
385
385
  self.pubsub.subscribe("tq_restart")
@@ -419,7 +419,7 @@ class WorkManager(object):
419
419
  except Exception as err:
420
420
  self.logger.exception(err)
421
421
 
422
- self.logger.info("waiting for {} running tasks, timeout: {}".format(self._running_count, timeout))
422
+ self.logger.info(f"waiting for {self._running_count} running tasks, timeout: {timeout}")
423
423
  time.sleep(1.0)
424
424
  self.__close()
425
425
  timeout_at = time.time() + timeout