specklia 1.9.101__py3-none-any.whl → 1.9.102__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.
specklia/client.py CHANGED
@@ -15,6 +15,7 @@ from shapely import MultiPolygon, Polygon, to_geojson
15
15
  from shapely.geometry import shape
16
16
 
17
17
  from specklia import chunked_transfer, utilities
18
+ from specklia.internal_admin_client import SpeckliaInternalAdminClient
18
19
 
19
20
  if TYPE_CHECKING:
20
21
  from datetime import datetime
@@ -44,6 +45,13 @@ class Specklia:
44
45
  url : str
45
46
  The url where Specklia is running, by default the URL of the Specklia server.
46
47
 
48
+ Attributes
49
+ ----------
50
+ user_id : str
51
+ The unique ID of the user associated with this client.
52
+ internal_admin : SpeckliaInternalAdminClient
53
+ Contains endpoints only accessible to internal Specklia administrators.
54
+
47
55
  Examples
48
56
  --------
49
57
  To start using Specklia, we first need to navigate to https://specklia.earthwave.co.uk and follow the
@@ -59,23 +67,48 @@ class Specklia:
59
67
  >>> client = Specklia(auth_token=user_auth_token)
60
68
  """
61
69
 
70
+ user_id: str
71
+ internal_admin: SpeckliaInternalAdminClient
72
+
62
73
  def __init__(self: Specklia, auth_token: str, url: str = "https://specklia-api.earthwave.co.uk") -> None:
63
74
  self.server_url = url
64
75
  self.auth_token = auth_token
65
76
  self._data_streaming_timeout_s = 300
77
+
78
+ # Internal admin routes are accessed through a separate object to keep the distinction clear.
79
+ self.internal_admin = SpeckliaInternalAdminClient(self._request)
80
+
66
81
  # immediately retrieve the user's ID. This serves as a check that their API token is valid.
67
82
  self._fetch_user_id()
68
83
 
69
84
  _log.info("New Specklia client created.")
70
85
 
86
+ def _request(
87
+ self: Specklia,
88
+ method: Literal["GET", "POST", "PUT", "DELETE"],
89
+ endpoint: str,
90
+ params: dict | None = None,
91
+ json: dict | None = None,
92
+ data: str | None = None,
93
+ ) -> requests.Response:
94
+ response = requests.request(
95
+ method,
96
+ self.server_url + "/" + endpoint,
97
+ headers={"Authorization": "Bearer " + self.auth_token},
98
+ params=params,
99
+ json=json,
100
+ data=data,
101
+ )
102
+ _check_response_ok(response)
103
+ return response
104
+
71
105
  def _fetch_user_id(self: Specklia) -> None:
72
106
  """
73
107
  Set the client's User ID.
74
108
 
75
109
  We've separated this out for testing reasons.
76
110
  """
77
- response = requests.post(self.server_url + "/users", headers={"Authorization": "Bearer " + self.auth_token})
78
- _check_response_ok(response)
111
+ response = self._request("POST", "users")
79
112
  self.user_id = response.json()
80
113
  _log.info("fetched User ID for client, was %s", self.user_id)
81
114
 
@@ -114,12 +147,7 @@ class Specklia:
114
147
  client.delete_user_from_group(), client.add_user_to_group(), and client.update_user_privileges to make any
115
148
  desired changes.
116
149
  """
117
- response = requests.get(
118
- self.server_url + "/users",
119
- headers={"Authorization": "Bearer " + self.auth_token},
120
- params={"group_id": group_id},
121
- )
122
- _check_response_ok(response)
150
+ response = self._request("GET", "users", params={"group_id": group_id})
123
151
  _log.info("listed users within group_id %s.", group_id)
124
152
  return pd.DataFrame(response.json()).convert_dtypes()
125
153
 
@@ -220,10 +248,7 @@ class Specklia:
220
248
  }
221
249
 
222
250
  # submit the query
223
- response = requests.post(
224
- self.server_url + "/query", data=json.dumps(request), headers={"Authorization": "Bearer " + self.auth_token}
225
- )
226
- _check_response_ok(response)
251
+ response = self._request("POST", "query", data=json.dumps(request))
227
252
 
228
253
  _log.info("queried dataset with ID %s.", dataset_id)
229
254
 
@@ -290,9 +315,8 @@ class Specklia:
290
315
 
291
316
  You must have READ_WRITE or ADMIN permissions within the group that owns the dataset in order to do this.
292
317
 
293
- Note that Ingests are temporarily restricted to those that have READ_WRITE permissions
294
- within the all_users group (i.e. Specklia is read-only to the general public).
295
- This restriction will be lifted once we have per-user billing in place for Specklia.
318
+ Note that Ingests are temporarily restricted to internal Specklia administrators (i.e. Specklia is read-only to
319
+ the general public). This restriction will be lifted once we have per-user billing in place for Specklia.
296
320
 
297
321
  Note that this can only be called up to 30,000 times per day for OLAP datasets - if you need to load more
298
322
  individual data files than this, ensure that you use this method on groups of files
@@ -331,17 +355,15 @@ class Specklia:
331
355
  )
332
356
  del n
333
357
 
334
- response = requests.post(
335
- self.server_url + "/ingest",
358
+ self._request(
359
+ "POST",
360
+ "ingest",
336
361
  json={
337
362
  "dataset_id": dataset_id,
338
363
  "new_points": upload_points,
339
364
  "duplicate_source_behaviour": duplicate_source_behaviour,
340
365
  },
341
- headers={"Authorization": "Bearer " + self.auth_token},
342
366
  )
343
- _check_response_ok(response)
344
-
345
367
  _log.info("Added new data to specklia dataset ID %s.", dataset_id)
346
368
 
347
369
  def delete_points_in_dataset(
@@ -369,22 +391,6 @@ class Specklia:
369
391
  _log.error("this method is not yet implemented.")
370
392
  raise NotImplementedError()
371
393
 
372
- def list_all_groups(self: Specklia) -> pd.DataFrame:
373
- """
374
- List all groups.
375
-
376
- You must have ADMIN permissions within the special all_users group in order to do this.
377
-
378
- Returns
379
- -------
380
- pd.DataFrame
381
- A dataframe describing all groups
382
- """
383
- response = requests.get(self.server_url + "/groups", headers={"Authorization": "Bearer " + self.auth_token})
384
- _check_response_ok(response)
385
- _log.info("listing all groups within Specklia.")
386
- return pd.DataFrame(response.json()).convert_dtypes()
387
-
388
394
  def create_group(self: Specklia, group_name: str) -> str:
389
395
  """
390
396
  Create a new Specklia group.
@@ -413,12 +419,7 @@ class Specklia:
413
419
  The endpoint will return the new group's unique ID, auto-generated by Specklia. We can pass this ID to other
414
420
  Specklia endpoints to modify the group, its members, and datasets.
415
421
  """
416
- response = requests.post(
417
- self.server_url + "/groups",
418
- json={"group_name": group_name},
419
- headers={"Authorization": "Bearer " + self.auth_token},
420
- )
421
- _check_response_ok(response)
422
+ response = self._request("POST", "groups", json={"group_name": group_name})
422
423
  _log.info("created new group with name %s.", group_name)
423
424
  return response.text.strip('\n"')
424
425
 
@@ -448,12 +449,11 @@ class Specklia:
448
449
 
449
450
  The group's unique ID, users, and datasets will remain unchanged.
450
451
  """
451
- response = requests.put(
452
- self.server_url + "/groups",
452
+ response = self._request(
453
+ "PUT",
454
+ "groups",
453
455
  json={"group_id": group_id, "new_group_name": new_group_name},
454
- headers={"Authorization": "Bearer " + self.auth_token},
455
456
  )
456
- _check_response_ok(response)
457
457
  _log.info("updated name of group ID %s to %s.", group_id, new_group_name)
458
458
  return response.text.strip('\n"')
459
459
 
@@ -482,12 +482,7 @@ class Specklia:
482
482
  The above will additionally delete any datasets owned by the group at the time of the deletion. Users within the
483
483
  group will be removed from it, but left unchanged otherwise.
484
484
  """
485
- response = requests.delete(
486
- self.server_url + "/groups",
487
- headers={"Authorization": "Bearer " + self.auth_token},
488
- params={"group_id": group_id},
489
- )
490
- _check_response_ok(response)
485
+ response = self._request("DELETE", "groups", params={"group_id": group_id})
491
486
  _log.info("deleted group ID %s", group_id)
492
487
  return response.text.strip('\n"')
493
488
 
@@ -517,10 +512,7 @@ class Specklia:
517
512
 
518
513
  We can now pass this ID to other Specklia endpoints to modify the group, its members, and datasets.
519
514
  """
520
- response = requests.get(
521
- self.server_url + "/groupmembership", headers={"Authorization": "Bearer " + self.auth_token}
522
- )
523
- _check_response_ok(response)
515
+ response = self._request("GET", "groupmembership")
524
516
  _log.info("listed groups that user is part of.")
525
517
  return pd.DataFrame(response.json()).convert_dtypes()
526
518
 
@@ -560,12 +552,11 @@ class Specklia:
560
552
  able to write to the group's datasets or manage users within the group, we can update their privileges via
561
553
  client.update_user_privileges().
562
554
  """
563
- response = requests.post(
564
- self.server_url + "/groupmembership",
555
+ response = self._request(
556
+ "POST",
557
+ "groupmembership",
565
558
  json={"group_id": group_id, "user_to_add_id": user_to_add_id},
566
- headers={"Authorization": "Bearer " + self.auth_token},
567
559
  )
568
- _check_response_ok(response)
569
560
  _log.info("added user ID %s to group ID %s", user_to_add_id, group_id)
570
561
  return response.text.strip('\n"')
571
562
 
@@ -621,12 +612,11 @@ class Specklia:
621
612
 
622
613
  We should always aim to grant users the lowest privileges necessary.
623
614
  """
624
- response = requests.put(
625
- self.server_url + "/groupmembership",
615
+ response = self._request(
616
+ "PUT",
617
+ "groupmembership",
626
618
  json={"group_id": group_id, "user_to_update_id": user_to_update_id, "new_privileges": new_privileges},
627
- headers={"Authorization": "Bearer " + self.auth_token},
628
619
  )
629
- _check_response_ok(response)
630
620
  _log.info(
631
621
  "Updated user ID %s privileges to %s within group ID %s.", user_to_update_id, new_privileges, group_id
632
622
  )
@@ -665,12 +655,11 @@ class Specklia:
665
655
  >>> client.delete_user_group_group(group_id=DETERMINED_GROUP_ID, user_to_delete_id=DETERMINED_USER_ID)
666
656
 
667
657
  """
668
- response = requests.delete(
669
- self.server_url + "/groupmembership",
670
- headers={"Authorization": "Bearer " + self.auth_token},
658
+ response = self._request(
659
+ "DELETE",
660
+ "groupmembership",
671
661
  params={"group_id": group_id, "user_to_delete_id": user_to_delete_id},
672
662
  )
673
- _check_response_ok(response)
674
663
  _log.info("Deleted user ID %s from group ID %s.", user_to_delete_id, group_id)
675
664
  return response.text.strip('\n"')
676
665
 
@@ -805,17 +794,16 @@ class Specklia:
805
794
  _log.warning(message)
806
795
  warnings.warn(message, stacklevel=1)
807
796
 
808
- response = requests.post(
809
- self.server_url + "/metadata",
797
+ response = self._request(
798
+ "POST",
799
+ "metadata",
810
800
  json={
811
801
  "dataset_name": dataset_name,
812
802
  "description": description,
813
803
  "columns": columns,
814
804
  "storage_technology": storage_technology,
815
805
  },
816
- headers={"Authorization": "Bearer " + self.auth_token},
817
806
  )
818
- _check_response_ok(response)
819
807
  _log.info("Created a new dataset with name '%s'", dataset_name)
820
808
  return response.text.strip('\n"')
821
809
 
@@ -860,12 +848,11 @@ class Specklia:
860
848
  ... group_id=important_group_id)
861
849
 
862
850
  """
863
- response = requests.put(
864
- self.server_url + "/metadata",
851
+ response = self._request(
852
+ "PUT",
853
+ "metadata",
865
854
  json={"dataset_id": dataset_id, "new_owning_group_id": new_owning_group_id},
866
- headers={"Authorization": "Bearer " + self.auth_token},
867
855
  )
868
- _check_response_ok(response)
869
856
  _log.info("set owning group for dataset ID %s to group ID %s", dataset_id, new_owning_group_id)
870
857
  return response.text.strip('\n"')
871
858
 
@@ -895,12 +882,7 @@ class Specklia:
895
882
  Specklia will respond with a success message as long as the dataset exists and we are an ADMIN within the
896
883
  group that owns it.
897
884
  """
898
- response = requests.delete(
899
- self.server_url + "/metadata",
900
- params={"dataset_id": dataset_id},
901
- headers={"Authorization": "Bearer " + self.auth_token},
902
- )
903
- _check_response_ok(response)
885
+ response = self._request("DELETE", "metadata", params={"dataset_id": dataset_id})
904
886
  _log.info("Deleted dataset with ID %s", dataset_id)
905
887
  return response.text.strip('\n"')
906
888
 
@@ -939,12 +921,7 @@ class Specklia:
939
921
  Example usage:
940
922
  >>> client.report_usage(dataset_id="GROUP_IP")
941
923
  """
942
- response = requests.get(
943
- self.server_url + "/usage",
944
- params={"group_id": group_id},
945
- headers={"Authorization": "Bearer " + self.auth_token},
946
- )
947
- _check_response_ok(response)
924
+ response = self._request("GET", "usage", params={"group_id": group_id})
948
925
  _log.info("Usage report queried for group_id %s", group_id)
949
926
  return response.json()
950
927
 
@@ -0,0 +1,74 @@
1
+ """Specklia internal admin client module."""
2
+
3
+ import logging
4
+ from typing import Callable, Literal, Self
5
+
6
+ import pandas as pd
7
+ import requests
8
+
9
+ _log = logging.getLogger(__name__)
10
+
11
+
12
+ class SpeckliaInternalAdminClient:
13
+ """Specklia client for endpoints only accessible to internal Specklia administrators.
14
+
15
+ Parameters
16
+ ----------
17
+ specklia_request : Callable
18
+ The function to use to make requests to Specklia internal admin endpoints.
19
+ """
20
+
21
+ def __init__(
22
+ self: Self,
23
+ specklia_request: Callable,
24
+ ) -> None:
25
+ self._specklia_request = specklia_request
26
+
27
+ def _request(
28
+ self: Self,
29
+ method: Literal["GET", "POST", "PUT", "DELETE"],
30
+ endpoint: str,
31
+ params: dict | None = None,
32
+ json: dict | None = None,
33
+ data: str | None = None,
34
+ ) -> requests.Response:
35
+ return self._specklia_request(
36
+ method=method,
37
+ endpoint=endpoint,
38
+ params=params,
39
+ json=json,
40
+ data=data,
41
+ )
42
+
43
+ def list_all_groups(self: Self) -> pd.DataFrame:
44
+ """
45
+ List all groups.
46
+
47
+ Returns
48
+ -------
49
+ pd.DataFrame
50
+ A dataframe describing all groups
51
+ """
52
+ response = self._request("GET", "groups")
53
+ _log.info("listing all groups within Specklia.")
54
+ return pd.DataFrame(response.json()).convert_dtypes()
55
+
56
+ def generate_user_api_key(self: Self, user_id: str) -> dict[str, str]:
57
+ """
58
+ Generate an API key for a user, creating the user if they do not already exist.
59
+
60
+ This will create the user if they do not already exist, and will replace any existing API key if present.
61
+
62
+ Parameters
63
+ ----------
64
+ user_id : str
65
+ The ID of the user to generate an API key for.
66
+
67
+ Returns
68
+ -------
69
+ dict[str, str]
70
+ A dictionary containing the `user_id` and the generated `token`.
71
+ """
72
+ response = self._request("PUT", "generate_user_api_key/" + user_id)
73
+ _log.info("Generated API key for user ID %s.", user_id)
74
+ return response.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: specklia
3
- Version: 1.9.101
3
+ Version: 1.9.102
4
4
  Summary: Python client for Specklia, a geospatial point cloud database by Earthwave.
5
5
  Home-page: https://specklia.earthwave.co.uk/
6
6
  Author: Earthwave Ltd
@@ -0,0 +1,10 @@
1
+ specklia/__init__.py,sha256=ePVHqq642NocoE8tS0cNTd0B5wJdUB7r3y815oQXD6A,51
2
+ specklia/chunked_transfer.py,sha256=pTm-x5Vwy9YtVTXcV7i0cYAo1LaSA_3qr1Of16R1u40,7732
3
+ specklia/client.py,sha256=oHL0-MbKoj9qMBKZOii_xEkby-RPPSjoZkPK6peNk3Q,40828
4
+ specklia/internal_admin_client.py,sha256=w3OyXjlWPivgd5lOmnISc6B6rjLbkGoRtmd2kO7lelA,2127
5
+ specklia/utilities.py,sha256=AjgDOM_UTDCY1QTb0yv83qXVuLSwi_CDKGs0vWen1oM,5087
6
+ specklia-1.9.102.dist-info/LICENCE,sha256=kjWTA-TtT_rJtsWuAgWvesvu01BytVXgt_uCbeQgjOg,1061
7
+ specklia-1.9.102.dist-info/METADATA,sha256=oHwCA1PqL7e1QgT3QErJXfCPARZ8AMo50jPQr5F5jg0,3083
8
+ specklia-1.9.102.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
9
+ specklia-1.9.102.dist-info/top_level.txt,sha256=XgU53UpAJbqEni5EjJaPdQPYuNx16Geg2I5A9lo1BQw,9
10
+ specklia-1.9.102.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- specklia/__init__.py,sha256=ePVHqq642NocoE8tS0cNTd0B5wJdUB7r3y815oQXD6A,51
2
- specklia/chunked_transfer.py,sha256=pTm-x5Vwy9YtVTXcV7i0cYAo1LaSA_3qr1Of16R1u40,7732
3
- specklia/client.py,sha256=6JYcjSpKtg_Lu2VnXAPwUuQuqUQF0ShvSuQU5Mk-p8c,42173
4
- specklia/utilities.py,sha256=AjgDOM_UTDCY1QTb0yv83qXVuLSwi_CDKGs0vWen1oM,5087
5
- specklia-1.9.101.dist-info/LICENCE,sha256=kjWTA-TtT_rJtsWuAgWvesvu01BytVXgt_uCbeQgjOg,1061
6
- specklia-1.9.101.dist-info/METADATA,sha256=Lm0Jf8dRPvUaXlwPM32ogrp2SH291mZSniB0t5ceWWo,3083
7
- specklia-1.9.101.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
8
- specklia-1.9.101.dist-info/top_level.txt,sha256=XgU53UpAJbqEni5EjJaPdQPYuNx16Geg2I5A9lo1BQw,9
9
- specklia-1.9.101.dist-info/RECORD,,