castor-extractor 0.5.0__py3-none-any.whl → 0.5.2__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 castor-extractor might be problematic. Click here for more details.

CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 - 2023-07-12
4
+ * Fix Metabase DbClient url
5
+
6
+ ## 0.5.1 - 2023-07-03
7
+ * Add support for Looker's `ContentViews`
8
+
3
9
  ## 0.5.0 - 2023-06-28
4
10
 
5
11
  * Stop supporting python3.7
@@ -17,7 +17,7 @@ from .retry import RetryStrategy, retry
17
17
  from .safe import SafeMode, safe_mode
18
18
  from .store import AbstractStorage, LocalStorage
19
19
  from .string import string_to_tuple
20
- from .time import current_date, current_datetime, current_timestamp
20
+ from .time import current_date, current_datetime, current_timestamp, past_date
21
21
  from .type import Callback, Getter, JsonType, SerializedAsset
22
22
  from .uri import uri_encode
23
23
  from .validation import validate_baseurl
@@ -1,4 +1,4 @@
1
- from datetime import date, datetime
1
+ from datetime import date, datetime, timedelta, timezone
2
2
 
3
3
 
4
4
  def current_datetime() -> datetime:
@@ -16,3 +16,23 @@ def current_timestamp() -> int:
16
16
  Returns the current timestamp from epoch (rounded to the nearest second)
17
17
  """
18
18
  return int(datetime.timestamp(current_datetime()))
19
+
20
+
21
+ def _set_uct_timezone(ts: datetime) -> datetime:
22
+ if ts.tzinfo is None:
23
+ return ts.replace(tzinfo=timezone.utc)
24
+ return ts
25
+
26
+
27
+ def now(tz: bool = False) -> datetime:
28
+ """
29
+ provide current time
30
+ optionally localize timezone
31
+ """
32
+ ts = datetime.utcnow()
33
+ return _set_uct_timezone(ts) if tz else ts
34
+
35
+
36
+ def past_date(past_days: int) -> date:
37
+ """returns a date in the past"""
38
+ return now().date() - timedelta(past_days)
@@ -1,11 +1,18 @@
1
+ import functools
1
2
  import logging
3
+ from datetime import date, timedelta
2
4
  from typing import Callable, Iterator, List, Optional, Sequence, Tuple
3
5
 
4
- from ....utils import Pager, PagerLogger, SafeMode, safe_mode
6
+ from dateutil.utils import today
7
+ from looker_sdk.sdk.api40.models import ContentView
8
+
9
+ from ....utils import Pager, PagerLogger, SafeMode, past_date, safe_mode
5
10
  from ..env import page_size
6
11
  from ..fields import format_fields
7
12
  from .constants import (
8
13
  CONNECTION_FIELDS,
14
+ CONTENT_VIEWS_FIELDS,
15
+ CONTENT_VIEWS_HISTORY_DAYS,
9
16
  DASHBOARD_FIELDS,
10
17
  FOLDER_FIELDS,
11
18
  GROUPS_HIERARCHY_FIELDS,
@@ -38,6 +45,20 @@ logger = logging.getLogger(__name__)
38
45
  OnApiCall = Callable[[], None]
39
46
 
40
47
 
48
+ def _mondays(history_depth_in_days: int) -> Iterator[date]:
49
+ """
50
+ Fetch all Mondays of the elapsed period
51
+ """
52
+ _MSG = "History depth must be strictly positive"
53
+ assert history_depth_in_days > 0, _MSG
54
+ current_date = past_date(history_depth_in_days)
55
+ end_date = today().date()
56
+ while current_date < end_date:
57
+ if current_date.weekday() == 0:
58
+ yield current_date
59
+ current_date += timedelta(days=1)
60
+
61
+
41
62
  class ApiPagerLogger(PagerLogger):
42
63
  def __init__(self, on_api_call: Optional[OnApiCall]):
43
64
  self._on_api_call = on_api_call
@@ -217,3 +238,30 @@ class ApiClient:
217
238
  )
218
239
  logger.info("All looker groups_roles fetched")
219
240
  return list(groups_roles)
241
+
242
+ def content_views(self) -> List[ContentView]:
243
+ """
244
+ List the number of views per {user x week x dashboard|look}
245
+ https://cloud.google.com/looker/docs/reference/looker-api/latest/types/ContentView
246
+ """
247
+ content_views: List[ContentView] = []
248
+
249
+ for day in _mondays(history_depth_in_days=CONTENT_VIEWS_HISTORY_DAYS):
250
+ formatted_day = day.strftime("%Y-%m-%d")
251
+ logger.info(f"Fetching content views for week {formatted_day}")
252
+
253
+ _fetch = functools.partial(
254
+ self._sdk.search_content_views,
255
+ fields=format_fields(CONTENT_VIEWS_FIELDS),
256
+ start_of_week_date=formatted_day,
257
+ user_id="NOT NULL",
258
+ )
259
+ look_views = list(_fetch(look_id="NOT NULL"))
260
+ dashboard_views = list(_fetch(dashboard_id="NOT NULL"))
261
+
262
+ content_views.extend(look_views + dashboard_views)
263
+
264
+ logger.info(
265
+ f"All looker content views fetched - {len(content_views)} rows"
266
+ )
267
+ return content_views
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  from unittest.mock import patch
2
3
 
3
4
  import pytest
@@ -5,6 +6,11 @@ from castor_extractor.visualization.looker import ( # type: ignore
5
6
  ApiClient,
6
7
  Credentials,
7
8
  )
9
+ from dateutil.utils import today
10
+ from freezegun import freeze_time
11
+ from source.packages.extractor.castor_extractor.visualization.looker.api.client import (
12
+ _mondays,
13
+ )
8
14
 
9
15
 
10
16
  def _credentials():
@@ -28,3 +34,32 @@ def test_api_client_has_admin_permissions(
28
34
  mock_init40.return_value = "sdk"
29
35
  client = ApiClient(_credentials())
30
36
  assert client._sdk == "sdk"
37
+
38
+
39
+ @freeze_time("2023-7-4")
40
+ def test__mondays():
41
+ expected_30_days = {
42
+ datetime.date(2023, 7, 3),
43
+ datetime.date(2023, 6, 26),
44
+ datetime.date(2023, 6, 19),
45
+ datetime.date(2023, 6, 12),
46
+ datetime.date(2023, 6, 5),
47
+ }
48
+ assert set(_mondays(history_depth_in_days=30)) == expected_30_days
49
+
50
+ # only mondays
51
+ assert all([day.weekday() == 0 for day in _mondays(30)])
52
+ assert all([day.weekday() == 0 for day in _mondays(100)])
53
+
54
+ # all days must remain in the elapsed history
55
+ history_days = 1_000
56
+ end = today().date()
57
+ start = end - datetime.timedelta(days=history_days)
58
+ output = _mondays(history_depth_in_days=history_days)
59
+
60
+ assert all([day >= start for day in output])
61
+ assert all([day < end for day in output])
62
+
63
+ with pytest.raises(AssertionError):
64
+ list(_mondays(history_depth_in_days=0))
65
+ list(_mondays(history_depth_in_days=-5))
@@ -208,6 +208,15 @@ GROUPS_ROLES_FIELDS = (
208
208
  },
209
209
  )
210
210
 
211
+ CONTENT_VIEWS_FIELDS = (
212
+ "dashboard_id",
213
+ "look_id",
214
+ "start_of_week_date",
215
+ "user_id",
216
+ "view_count",
217
+ )
218
+ CONTENT_VIEWS_HISTORY_DAYS = 30
219
+
211
220
 
212
221
  # Model from looker
213
222
  LOOKML_PROJECT_NAME_BLOCKLIST = ("looker-data", "system__activity")
@@ -4,13 +4,14 @@ from enum import Enum
4
4
  class LookerAsset(Enum):
5
5
  """Looker assets"""
6
6
 
7
+ CONNECTIONS = "connections"
8
+ CONTENT_VIEWS = "content_views"
7
9
  DASHBOARDS = "dashboards"
8
10
  EXPLORES = "explores"
9
11
  FOLDERS = "folders"
10
- LOOKS = "looks"
11
- LOOKML_MODELS = "lookml_models"
12
- USERS = "users"
13
- CONNECTIONS = "connections"
14
- PROJECTS = "projects"
15
12
  GROUPS_HIERARCHY = "groups_hierarchy"
16
13
  GROUPS_ROLES = "groups_roles"
14
+ LOOKML_MODELS = "lookml_models"
15
+ LOOKS = "looks"
16
+ PROJECTS = "projects"
17
+ USERS = "users"
@@ -80,6 +80,10 @@ def iterate_all_data(
80
80
  groups_roles = client.groups_roles()
81
81
  yield LookerAsset.GROUPS_ROLES, deep_serialize(groups_roles)
82
82
 
83
+ logger.info("Extracting content views from Looker API")
84
+ content_views = client.content_views()
85
+ yield LookerAsset.CONTENT_VIEWS, deep_serialize(content_views)
86
+
83
87
 
84
88
  def extract_all(**kwargs) -> None:
85
89
  """
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
19
19
 
20
- PG_URL = "postgresql://{username}:{password}@{host}:{port}/{database}"
20
+ PG_URL = "postgresql://{user}:{password}@{host}:{port}/{database}"
21
21
  SQL_FILE_PATH = "queries/{name}.sql"
22
22
 
23
23
  ENCRYPTION_SECRET_KEY = "CASTOR_METABASE_ENCRYPTION_SECRET_KEY"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: castor-extractor
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Extract your metadata assets.
5
5
  Home-page: https://www.castordoc.com/
6
6
  License: EULA
@@ -1,4 +1,4 @@
1
- CHANGELOG.md,sha256=JyLIQXxoZhPRtgV6sNO8zNWIV5Y3DfagWytL-MpT1Dg,5240
1
+ CHANGELOG.md,sha256=ud3JHRlNCrRF1T77IUh5tSEI1ezW8RqcWzYWAngh1m4,5356
2
2
  LICENCE,sha256=sL-IGa4hweyya1HgzMskrRdybbIa2cktzxb5qmUgDg8,8254
3
3
  README.md,sha256=EL6JpZxvaQFOYv5WFuSjZvSk9Hcpsf7alMlUC5IPFjA,3423
4
4
  castor_extractor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -43,7 +43,7 @@ castor_extractor/uploader/env_test.py,sha256=ClCWWtwd2N-5ClIDUxVMeKkWfhhOTxpppsX
43
43
  castor_extractor/uploader/upload.py,sha256=NTXF4BlVEsRCA6S-SunyNHkcXN9JaRpK1W65ZeMWAcw,3353
44
44
  castor_extractor/uploader/upload_test.py,sha256=WzGtYoe45-Q_AAR_gj7aOZU_C342GdyZxiUHwITi2oU,329
45
45
  castor_extractor/uploader/utils.py,sha256=NCe0tkB28BVhqzOaDhDjaSfODjjcPWB17X6chnvyCWs,478
46
- castor_extractor/utils/__init__.py,sha256=R8P4994ZN8E7O1uavThDAP1qQO73eii9RfnWOsq3UKE,918
46
+ castor_extractor/utils/__init__.py,sha256=cENngVhgk311f4xvmaRlqO5cu5lERTrdZm0r_bJCrJk,929
47
47
  castor_extractor/utils/collection.py,sha256=uenJvfamphxV5ZFt12BgfsRs99pWffYJIMjAD_Laz2Q,417
48
48
  castor_extractor/utils/constants.py,sha256=qBQprS9U66mS-RIBXiLujdTSV3WvGv40Bc0khP4Abdk,39
49
49
  castor_extractor/utils/deprecate.py,sha256=J-UzSNPGyIBW0oAEnQae6zLL6kgqUNmjtrS0K21fb6s,818
@@ -66,7 +66,7 @@ castor_extractor/utils/safe_test.py,sha256=IHN1Z761tYMFslYC-2HAfkXmFPh4LYSqNLs4Q
66
66
  castor_extractor/utils/store.py,sha256=mlfg0sS4azJ1eg28D_cWUWkF-npy3R9d4VPC4EAVNnI,2092
67
67
  castor_extractor/utils/string.py,sha256=-fXrp4CLJoECwOaYTuv2d8vatA74EI22URschGD8pvo,1961
68
68
  castor_extractor/utils/string_test.py,sha256=5n4leOBxphtnyD0iHajzsTog4vLlYk8h8LDYV4ZMSeM,2206
69
- castor_extractor/utils/time.py,sha256=8eTRskem2HLbhTGoNPB9R_enKu9ZwvTHMSW8UkxZe2g,430
69
+ castor_extractor/utils/time.py,sha256=yQC2dOTadqPnOgk-50OJ2Nt_0wD3ySU8XuwdXFWIiNU,903
70
70
  castor_extractor/utils/type.py,sha256=87t32cTctEjX-_BqZLtPLWu-M9OVvw_lFU4DbaQ6V0U,313
71
71
  castor_extractor/utils/uri.py,sha256=jmP9hY-6PRqdc3-vAOdtll_U6q9VCqSqmBAN6QRs3ZI,150
72
72
  castor_extractor/utils/uri_test.py,sha256=1XKF6qSseCeD4G4ckaNO07JXfGbt7XUVinOZdpEYrDQ,259
@@ -76,16 +76,16 @@ castor_extractor/utils/write.py,sha256=CbLMz-mkUFduwogERwe69GXXVO65HEDirVm-kDJkR
76
76
  castor_extractor/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  castor_extractor/visualization/looker/__init__.py,sha256=F1DkZQStdmPRc2AwurWL0Hhaz2SRW9ERfZLvMcBJc0g,191
78
78
  castor_extractor/visualization/looker/api/__init__.py,sha256=eMtzoMAE7ZnvfwJJAOh9Cooztj4Juzc40wSEHhIYXw4,181
79
- castor_extractor/visualization/looker/api/client.py,sha256=iagryhCNvYAlPTpPmQQx4L6WrHVLSDjchb-rMpbb0cc,7137
80
- castor_extractor/visualization/looker/api/client_test.py,sha256=hww1Yj38lndkxUtF40oRfA3bTWkA47xMQhVS-22HZDc,816
81
- castor_extractor/visualization/looker/api/constants.py,sha256=urLKuLQ6HXX1xeD5EVZArd9ENYZJ0EMrSP92b-MJIso,3895
79
+ castor_extractor/visualization/looker/api/client.py,sha256=V-TcUCU65BTMUbXInMP5SQKuQDbylpFc9af4O5KyjL8,8859
80
+ castor_extractor/visualization/looker/api/client_test.py,sha256=LlQCI_Yuu4DWmT1_lcgZP6-BZMCzgz9--F9TV0lrPCk,1924
81
+ castor_extractor/visualization/looker/api/constants.py,sha256=__ExzGkhnVadOeIplEgHixqzM4lmZWiOZnC0GRZXLTg,4049
82
82
  castor_extractor/visualization/looker/api/sdk.py,sha256=MMHFABzpWg84pSVDqYvaDxjKB50Pr7ttNUqrcdtR-po,3526
83
83
  castor_extractor/visualization/looker/api/sdk_test.py,sha256=NHtKZTflPhqzBFHs1TyAQaubgxfzLLwYKFT8rEqR55I,1742
84
84
  castor_extractor/visualization/looker/api/utils.py,sha256=vWSz6iFFF4uA3Y3eqqGEH1CkXcZbtHqVFMnPS2o8LCQ,1320
85
- castor_extractor/visualization/looker/assets.py,sha256=HF_gXewUsdztSr9v7-Z-FTWmVzf1Fwn0Tf-8BIvgIfE,364
85
+ castor_extractor/visualization/looker/assets.py,sha256=50Tr3RrWzbGZCpUMkNK42wnBJMUkKdlCW1IT2KHyt3A,400
86
86
  castor_extractor/visualization/looker/constant.py,sha256=ZqgUJxsSkyoyA7fM5fHKNkPDA2ryr34gG97dmgSgB9A,467
87
87
  castor_extractor/visualization/looker/env.py,sha256=spfVc6E2CdYDdrPepX-YEUUfHbq-ZlueU9q4pxiaRPQ,864
88
- castor_extractor/visualization/looker/extract.py,sha256=XJq8fz5WWn-CAg0haoDJXfj6tlgrBcUwxjfBjbxT7oU,3514
88
+ castor_extractor/visualization/looker/extract.py,sha256=C6lBh6b9KbYjkgUigxMWE4af7ameCBQ9nSvpG8OKcTE,3685
89
89
  castor_extractor/visualization/looker/fields.py,sha256=WmiSehmczWTufCLg4r2Ozq2grUpzxDNvIAHyGuOoGs4,636
90
90
  castor_extractor/visualization/looker/fields_test.py,sha256=7Cwq8Qky6aTZg8nCHp1gmPJtd9pGNB4QeMIRRWdHo5w,782
91
91
  castor_extractor/visualization/looker/parameters.py,sha256=JZ5o5BgRs4SxxX5artUfhPaQBX6YPfX8wRp1m_txXUs,1527
@@ -96,7 +96,7 @@ castor_extractor/visualization/metabase/client/api/__init__.py,sha256=bWs7daLw2N
96
96
  castor_extractor/visualization/metabase/client/api/client.py,sha256=n4jV7jjLY4meM2sKsqtHXbw76sNlbeUKd1mbLU_GbeY,5609
97
97
  castor_extractor/visualization/metabase/client/api/credentials.py,sha256=HsyJ3X_N-smw_6cwQdi9pQ2Pl3KWoBqZL_Xg8dy7hGo,1334
98
98
  castor_extractor/visualization/metabase/client/db/__init__.py,sha256=xmqEI59hvioLxE8LXcH4_njCrBy8_3QjIrZNb6bW6Rs,29
99
- castor_extractor/visualization/metabase/client/db/client.py,sha256=h8s4mYoN_wsPR7kA7SZBT-f5b_RgJcoJWy1lA-_VM0g,4095
99
+ castor_extractor/visualization/metabase/client/db/client.py,sha256=yXjHVeuh-GRFA3rUHMVtOHFhcxxnp_0TDpBpI8UISLA,4091
100
100
  castor_extractor/visualization/metabase/client/db/credentials.py,sha256=xWTQ6xDBJak7YS13B5kmh26L36VY0XYpCX_DbEAB4QM,1863
101
101
  castor_extractor/visualization/metabase/client/db/queries/.sqlfluff,sha256=sOQQOpAa9QMj9cBlulfmt-DZ_kQzMpzSAEnh10QGSB0,76
102
102
  castor_extractor/visualization/metabase/client/db/queries/base_url.sql,sha256=p2EL9kdt-hw_yh3aeCE91AXEB4RrYAbG2QrBBNqQjDE,79
@@ -273,7 +273,7 @@ castor_extractor/warehouse/synapse/queries/schema.sql,sha256=aX9xNrBD_ydwl-znGSF
273
273
  castor_extractor/warehouse/synapse/queries/table.sql,sha256=mCE8bR1Vb7j7SwZW2gafcXidQ2fo1HwxcybA8wP2Kfs,1049
274
274
  castor_extractor/warehouse/synapse/queries/user.sql,sha256=sTb_SS7Zj3AXW1SggKPLNMCd0qoTpL7XI_BJRMaEpBg,67
275
275
  castor_extractor/warehouse/synapse/queries/view_ddl.sql,sha256=3EVbp5_yTgdByHFIPLHmnoOnqqLE77SrjAwFDvu4e54,249
276
- castor_extractor-0.5.0.dist-info/METADATA,sha256=ZE51CdA4LsLr6kVHK5kgs3v5lLAtPgJoDrg-VYMUXTM,6308
277
- castor_extractor-0.5.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
278
- castor_extractor-0.5.0.dist-info/entry_points.txt,sha256=q_a-iEB9OZF05IUsF43OTLYvgEbHFGp52u9lWtWf9rU,981
279
- castor_extractor-0.5.0.dist-info/RECORD,,
276
+ castor_extractor-0.5.2.dist-info/METADATA,sha256=WOnF0a8bvVOGTD1yRdLEHhMr2aZV1QZFbMCSRLrG6VU,6308
277
+ castor_extractor-0.5.2.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
278
+ castor_extractor-0.5.2.dist-info/entry_points.txt,sha256=q_a-iEB9OZF05IUsF43OTLYvgEbHFGp52u9lWtWf9rU,981
279
+ castor_extractor-0.5.2.dist-info/RECORD,,