pyspiral 0.2.5__cp310-abi3-macosx_11_0_arm64.whl → 0.4.0__cp310-abi3-macosx_11_0_arm64.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.
Files changed (114) hide show
  1. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/METADATA +12 -14
  2. pyspiral-0.4.0.dist-info/RECORD +98 -0
  3. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/WHEEL +1 -1
  4. spiral/__init__.py +6 -7
  5. spiral/_lib.abi3.so +0 -0
  6. spiral/adbc.py +21 -14
  7. spiral/api/__init__.py +15 -172
  8. spiral/api/admin.py +12 -26
  9. spiral/api/client.py +160 -0
  10. spiral/api/filesystems.py +100 -72
  11. spiral/api/organizations.py +45 -58
  12. spiral/api/projects.py +171 -134
  13. spiral/api/telemetry.py +19 -0
  14. spiral/api/types.py +20 -0
  15. spiral/api/workloads.py +32 -25
  16. spiral/{arrow.py → arrow_.py} +12 -0
  17. spiral/cli/__init__.py +2 -5
  18. spiral/cli/admin.py +7 -12
  19. spiral/cli/app.py +23 -6
  20. spiral/cli/console.py +1 -1
  21. spiral/cli/fs.py +83 -18
  22. spiral/cli/iceberg/__init__.py +7 -0
  23. spiral/cli/iceberg/namespaces.py +47 -0
  24. spiral/cli/iceberg/tables.py +60 -0
  25. spiral/cli/indexes/__init__.py +19 -0
  26. spiral/cli/login.py +14 -5
  27. spiral/cli/orgs.py +90 -0
  28. spiral/cli/printer.py +9 -1
  29. spiral/cli/projects.py +136 -0
  30. spiral/cli/state.py +2 -0
  31. spiral/cli/tables/__init__.py +121 -0
  32. spiral/cli/telemetry.py +18 -0
  33. spiral/cli/types.py +8 -10
  34. spiral/cli/{workload.py → workloads.py} +11 -11
  35. spiral/{catalog.py → client.py} +22 -21
  36. spiral/core/client/__init__.pyi +117 -0
  37. spiral/core/index/__init__.pyi +15 -0
  38. spiral/core/table/__init__.pyi +108 -0
  39. spiral/core/{manifests → table/manifests}/__init__.pyi +5 -23
  40. spiral/core/table/metastore/__init__.pyi +62 -0
  41. spiral/core/{spec → table/spec}/__init__.pyi +49 -92
  42. spiral/datetime_.py +27 -0
  43. spiral/expressions/__init__.py +40 -17
  44. spiral/expressions/base.py +5 -5
  45. spiral/expressions/list_.py +1 -1
  46. spiral/expressions/mp4.py +62 -0
  47. spiral/expressions/png.py +18 -0
  48. spiral/expressions/qoi.py +18 -0
  49. spiral/expressions/refs.py +23 -9
  50. spiral/expressions/struct.py +7 -5
  51. spiral/expressions/text.py +62 -0
  52. spiral/expressions/tiff.py +88 -88
  53. spiral/expressions/udf.py +3 -3
  54. spiral/iceberg/__init__.py +3 -0
  55. spiral/iceberg/client.py +33 -0
  56. spiral/indexes/__init__.py +5 -0
  57. spiral/indexes/client.py +137 -0
  58. spiral/indexes/index.py +34 -0
  59. spiral/indexes/scan.py +22 -0
  60. spiral/project.py +19 -110
  61. spiral/{proto → protogen}/_/scandal/__init__.py +32 -77
  62. spiral/protogen/_/spiral/table/__init__.py +22 -0
  63. spiral/protogen/substrait/__init__.py +3399 -0
  64. spiral/protogen/substrait/extensions/__init__.py +115 -0
  65. spiral/server.py +17 -0
  66. spiral/settings.py +31 -87
  67. spiral/substrait_.py +10 -6
  68. spiral/tables/__init__.py +12 -0
  69. spiral/tables/client.py +130 -0
  70. spiral/{dataset.py → tables/dataset.py} +36 -25
  71. spiral/tables/debug/manifests.py +70 -0
  72. spiral/tables/debug/metrics.py +56 -0
  73. spiral/{debug.py → tables/debug/scan.py} +6 -9
  74. spiral/tables/maintenance.py +12 -0
  75. spiral/tables/scan.py +193 -0
  76. spiral/tables/snapshot.py +78 -0
  77. spiral/tables/table.py +157 -0
  78. spiral/tables/transaction.py +52 -0
  79. pyspiral-0.2.5.dist-info/RECORD +0 -81
  80. spiral/api/tables.py +0 -94
  81. spiral/api/tokens.py +0 -56
  82. spiral/authn/authn.py +0 -89
  83. spiral/authn/device.py +0 -206
  84. spiral/authn/github_.py +0 -33
  85. spiral/authn/modal_.py +0 -18
  86. spiral/cli/org.py +0 -90
  87. spiral/cli/project.py +0 -107
  88. spiral/cli/table.py +0 -20
  89. spiral/cli/token.py +0 -27
  90. spiral/config.py +0 -26
  91. spiral/core/core/__init__.pyi +0 -53
  92. spiral/core/metastore/__init__.pyi +0 -91
  93. spiral/proto/_/spfs/__init__.py +0 -36
  94. spiral/proto/_/spiral/table/__init__.py +0 -225
  95. spiral/proto/_/spiraldb/metastore/__init__.py +0 -499
  96. spiral/proto/__init__.py +0 -0
  97. spiral/proto/scandal/__init__.py +0 -45
  98. spiral/proto/spiral/__init__.py +0 -0
  99. spiral/proto/spiral/table/__init__.py +0 -96
  100. spiral/scan_.py +0 -168
  101. spiral/table.py +0 -157
  102. {pyspiral-0.2.5.dist-info → pyspiral-0.4.0.dist-info}/entry_points.txt +0 -0
  103. /spiral/{authn/__init__.py → core/__init__.pyi} +0 -0
  104. /spiral/{core → protogen/_}/__init__.py +0 -0
  105. /spiral/{proto/_ → protogen/_/arrow}/__init__.py +0 -0
  106. /spiral/{proto/_/arrow → protogen/_/arrow/flight}/__init__.py +0 -0
  107. /spiral/{proto/_/arrow/flight → protogen/_/arrow/flight/protocol}/__init__.py +0 -0
  108. /spiral/{proto → protogen}/_/arrow/flight/protocol/sql/__init__.py +0 -0
  109. /spiral/{proto/_/arrow/flight/protocol → protogen/_/spiral}/__init__.py +0 -0
  110. /spiral/{proto → protogen/_}/substrait/__init__.py +0 -0
  111. /spiral/{proto → protogen/_}/substrait/extensions/__init__.py +0 -0
  112. /spiral/{proto/_/spiral → protogen}/__init__.py +0 -0
  113. /spiral/{proto → protogen}/util.py +0 -0
  114. /spiral/{proto/_/spiraldb → tables/debug}/__init__.py +0 -0
@@ -1,81 +0,0 @@
1
- pyspiral-0.2.5.dist-info/METADATA,sha256=v9fKmJB3Y9Qss3pzr-rtw4PGnry0iBaFLN3_fwefN2s,1699
2
- pyspiral-0.2.5.dist-info/WHEEL,sha256=j3ku1HwtRttgdyoybPiqmsz03FP6lDUkPQNFM63xZJo,103
3
- pyspiral-0.2.5.dist-info/entry_points.txt,sha256=uft7u-a6g40NLt4Q6BleWbK4NY0M8nZuYPpP8DV0EOk,45
4
- spiral/catalog.py,sha256=BtthmRApU1RSb6KbUfVTM2aYeLsnlO0nKDYHBYhdr9M,2496
5
- spiral/scan_.py,sha256=rbLl85yeOkRuLsbk6QKng0_U4gtGGsW6C-aJiJSxuv8,5946
6
- spiral/substrait_.py,sha256=5ZXnYcsXEdrBogECnoL6IMlsjsseYHEnVARgRpy2vt8,12671
7
- spiral/config.py,sha256=ovtE5D3r6_g90ZRDJhlJyWhOBlxLjagvomOyA-VZmdc,911
8
- spiral/core/core/__init__.pyi,sha256=-OCtTgqjUN7sACAmgrAA_TrqmuP7epNSyL9XjqnNTa4,1914
9
- spiral/core/spec/__init__.pyi,sha256=zwOPhpBS_iOrPkOdc4ySpgzICZNbteMZf4c2wdkWw1Y,7251
10
- spiral/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- spiral/core/manifests/__init__.pyi,sha256=DXr4Ab_Xo11AzrJlSj7FTSJc8qoO-SkL-_ik4kB855U,1516
12
- spiral/core/metastore/__init__.pyi,sha256=pdKED91GVJ9XWxTWc9gwkHvVHV_RKxvL43Ofs5ndmew,3145
13
- spiral/types_.py,sha256=W_jyO7F6rpPiH69jhgSgV7OxQZbOlb1Ho3InpKUP6Eo,155
14
- spiral/proto/scandal/__init__.py,sha256=wAAEkPN4S4XDpGQtw1MV5zFUeHM01XSAa5tgUgcALvg,777
15
- spiral/proto/util.py,sha256=smnvVo6nYH3FfDm9jqhNLaXz4bbTBaQezHQDCTvZyiQ,1486
16
- spiral/proto/spiral/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- spiral/proto/spiral/table/__init__.py,sha256=_F1f52RMkZsXofPXpJb2KE8KR5l6zxCtrGrabR1uDxo,2816
18
- spiral/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- spiral/proto/_/scandal/__init__.py,sha256=rQJdbN3UKDJ8vOJ5V7l3KumNHlRyY8iw25HCLsIDB4I,6582
20
- spiral/proto/_/arrow/flight/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- spiral/proto/_/arrow/flight/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- spiral/proto/_/arrow/flight/protocol/sql/__init__.py,sha256=_xhj9QkWEW1qZ-iVxcQ8k4EjYr7KJ5ofitJGqVUGQi4,79921
23
- spiral/proto/_/arrow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- spiral/proto/_/spiral/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
- spiral/proto/_/spiral/table/__init__.py,sha256=sjK2dmvB09PqV3lxKMEk5QoHjC37HMW0MnxR1QDuBg0,7387
26
- spiral/proto/_/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- spiral/proto/_/spiraldb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- spiral/proto/_/spiraldb/metastore/__init__.py,sha256=40Egtg8MRYTaTTYRKOHkwuiyXEkw3Yg7ETCQskIzpIg,16873
29
- spiral/proto/_/spfs/__init__.py,sha256=9WtIXr7HGslKWRHHieFDo8N_qnGL4QQyLOCWEkOKRvk,1017
30
- spiral/proto/substrait/__init__.py,sha256=pV4-T-lwAHKkfFrNYSUGY4IkbIvuKjSo_imzF7BLj_s,126526
31
- spiral/proto/substrait/extensions/__init__.py,sha256=yD7dg0TBqn-GK_L0qeVof1GKnwSLg_kPyQSV3kcSljs,3655
32
- spiral/arrow.py,sha256=LBPwZcGkP4kXb42_kl5IUwWW3DO84CV3QJDwCHjG5Dg,7225
33
- spiral/__init__.py,sha256=4EiQkY17qHT9dpxu41fdOV1kqGl_b-HXNRQg--ZwJbo,286
34
- spiral/cli/console.py,sha256=-OP0bB_efxhWh4lZ95KRdu-SRgSUMJ47Rbi9FHv1TlY,2577
35
- spiral/cli/org.py,sha256=ezWhoGUkJQQAwI1jKvDP8uZPNlnou_hXtRDa1us5cSE,2935
36
- spiral/cli/token.py,sha256=dv30aa745bbS-c3tyzQUTSxGG_N0kCt8G4bip9fP_EM,968
37
- spiral/cli/__init__.py,sha256=CoiAJ7FDgqjG_TrU-6SpP1hyZloIlEP4wcwnrF8flHM,2237
38
- spiral/cli/types.py,sha256=4cphJs-i0vfq_CcnHxT9FpHiZdGwxME5GnOmIGB6Thw,1436
39
- spiral/cli/workload.py,sha256=-XreFPJcX7kZvcYE3oQMeGkkYoXi25R5nuktJPg-PuY,2000
40
- spiral/cli/admin.py,sha256=3pIs6PxDugMtdgzfRpn_HfBDv-cwAYtF0cJP2INB01A,579
41
- spiral/cli/fs.py,sha256=8sQAgMahAq0gtXJqnuiKUkx4sO6vEEF4Iaq_-AF3wro,1524
42
- spiral/cli/app.py,sha256=2oZfDTgj_gZ-lFMMzzJJTnvVzQhp_iedvH-FJnaaMW0,1487
43
- spiral/cli/table.py,sha256=eh2NAk0GlfvthwRNeIbcZTsRWU3ypFx_uu9OaOLHPUo,628
44
- spiral/cli/login.py,sha256=C7VpqVyYO2daUeIWHoelWnSGN7cju8YEuqOy12ImH4c,381
45
- spiral/cli/printer.py,sha256=5HD3UcszFfPk-dK8U5akuvtXqMB7PMgOB1DFYMqspG8,1625
46
- spiral/cli/__main__.py,sha256=kNaKM2xgJo7GRogf83nYldLM-RGUR6vymdGwZxywQu0,71
47
- spiral/cli/project.py,sha256=aCxvw8UVwSmh_anArizIQ3_pLVT6QH9jYwMsGfpJUgM,4486
48
- spiral/cli/state.py,sha256=1quvei8TnDTT6mDRo58P8FUfy4w16Z9sggBz7cFgllY,70
49
- spiral/dataset.py,sha256=jUeXvE4B5nKh9VNmHxvROQbEkTUxqyYS8oS6EQyRbng,7555
50
- spiral/grpc_.py,sha256=f3czdP1Mxme42Y5--a5ogYq1TTiWn-J_MlGjwJ2mWwM,1015
51
- spiral/debug.py,sha256=t590eAUtNWwMTsSdkjVNN7J1iMqY2p4PRJ3BWR_ozho,8999
52
- spiral/expressions/tiff.py,sha256=k5GMzm4FmGBJMyja-D6u_kt_-vHYIdj4bnYYZut5lK4,7579
53
- spiral/expressions/io.py,sha256=gJ2a0FKMmdxarWKENulPRwH7KDvSJTIh_OUxX306xAM,3045
54
- spiral/expressions/__init__.py,sha256=T2POn0Z-mQj9PY8ukYWYn5A832Fz7Y6F8RRASo6Xl3A,5638
55
- spiral/expressions/list_.py,sha256=nbo4xQAuqBsQGajq_JgORaJl8_CDvOAv14zMbqmtZh4,1814
56
- spiral/expressions/http.py,sha256=begUydWoFHEqjeLkATvI_v66Ez6_rR-OQBWO5cHbb9c,2742
57
- spiral/expressions/refs.py,sha256=F3xr7wmrbAawMkLTWcvVwczbOMNOnIttSMRDuzjZHbo,1774
58
- spiral/expressions/udf.py,sha256=vOlrdxiVpt7vdSgiTKX_XR86YKyu02Fdwb9xlINCby4,1363
59
- spiral/expressions/str_.py,sha256=tY8RXW3JWvr1-bEfCZtk5FAf11wKJnXPuA9EoeJ9tA4,1265
60
- spiral/expressions/base.py,sha256=mTBwS6CwdDaV8uotjZUiKi7GHQzX2TRPpnseJJUDrR0,4776
61
- spiral/expressions/struct.py,sha256=MuxoBP6ESpwmjzusG-_HxHGYKvQQz6AZWzvw7vNUHJM,2007
62
- spiral/settings.py,sha256=e_F6GQea6ljXzCRgFxMBMecoGhFpEhePhMRQkYOoVO4,4555
63
- spiral/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- spiral/api/filesystems.py,sha256=NPf5PCyCQ7eEU723fOdKcuGPgn83pthkb_jJ9aB0fwM,3431
65
- spiral/api/workloads.py,sha256=4MWs2pp9AWvx6cZhgyW-ehyCRxpHc_NAQgHaoYOEBfg,1266
66
- spiral/api/__init__.py,sha256=Ub3IYeQUJ_z0-Y_SXmNasxb6uefKynQkuUZgRM1enyc,6660
67
- spiral/api/tokens.py,sha256=WaSRr_l3i81t5u2qi3kWW-fySbyKFm-PQK1ZlmoSByc,1464
68
- spiral/api/admin.py,sha256=HJBrRJScbcdDuFhF_06E0EyE-_Y0osfYPxVoRAyEoTc,837
69
- spiral/api/organizations.py,sha256=-PO93HTX02IxhXM6SJpAhnAXpVW1WjthFO4-AOzZAC4,2670
70
- spiral/api/tables.py,sha256=uPoWkkcW7lJUxU0fUgDK5Gy_DT8y7gJFXdgxQpqcc5w,2548
71
- spiral/api/projects.py,sha256=-VGlu5V3TJ3XLCGu85bPHRFiktIADnAxfLWIH8Rmxug,4986
72
- spiral/table.py,sha256=iJ-Lhu9ieSNg728cvUNRERcxKz7pj8hMCXPGBFHg7tI,4845
73
- spiral/authn/modal_.py,sha256=agcnR3dYTslkH2K_a2Eis_2JWn9Ps11FVrGG_jkOdGk,472
74
- spiral/authn/device.py,sha256=ohHSVLW3a-qLNYQGN-3kXxV_836xOe0UYBN8i63cqAQ,6796
75
- spiral/authn/authn.py,sha256=OCGJAUfoKLiXw9xAcAnX6i6mBRlvsl6qEFcimqQOu7g,2555
76
- spiral/authn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
- spiral/authn/github_.py,sha256=K-0RUHDreINjnCDHyT9aeVDRk6WtNP7noBYEcwdz2W4,1313
78
- spiral/adbc.py,sha256=H_bzevPy5teyZKzjczh1gQ_zPcfk5sNASiJKQvyab9E,13830
79
- spiral/project.py,sha256=q9jHql7hz5OQzPfDfvE_hN3K9cZotD0cxkdug9EUTwM,4889
80
- spiral/_lib.abi3.so,sha256=Te1P4F2PJPY2E7qn69_ApKZwKkIp93sdpf21XZJZ6jU,61005104
81
- pyspiral-0.2.5.dist-info/RECORD,,
spiral/api/tables.py DELETED
@@ -1,94 +0,0 @@
1
- from typing import Annotated
2
-
3
- from pydantic import (
4
- AfterValidator,
5
- BaseModel,
6
- StringConstraints,
7
- )
8
-
9
- from . import ArrowSchema, Paged, PagedRequest, PagedResponse, ProjectId, ServiceBase
10
-
11
-
12
- def _validate_root_uri(uri: str) -> str:
13
- if uri.endswith("/"):
14
- raise ValueError("Root URI must not end with a slash.")
15
- return uri
16
-
17
-
18
- RootUri = Annotated[str, AfterValidator(_validate_root_uri)]
19
- DatasetName = Annotated[str, StringConstraints(max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]+$")]
20
- TableName = Annotated[str, StringConstraints(max_length=128, pattern=r"^[a-zA-Z_][a-zA-Z0-9_-]+$")]
21
-
22
-
23
- class TableMetadata(BaseModel):
24
- key_schema: ArrowSchema
25
- root_uri: RootUri
26
- spfs_mount_id: str | None = None
27
-
28
- # TODO(marko): Randomize this on creation of metadata.
29
- # Column group salt is used to compute column group IDs.
30
- # It's used to ensure that column group IDs are unique
31
- # across different tables, even if paths are the same.
32
- # It's never modified.
33
- column_group_salt: int = 0
34
-
35
-
36
- class Table(BaseModel):
37
- id: str
38
- project_id: ProjectId
39
- dataset: DatasetName
40
- table: TableName
41
- metadata: TableMetadata
42
-
43
-
44
- class CreateTable:
45
- class Request(BaseModel):
46
- project_id: ProjectId
47
- dataset: DatasetName
48
- table: TableName
49
- key_schema: ArrowSchema
50
- root_uri: RootUri | None = None
51
- exist_ok: bool = False
52
-
53
- class Response(BaseModel):
54
- table: Table
55
-
56
-
57
- class FindTable:
58
- class Request(BaseModel):
59
- project_id: ProjectId
60
- dataset: DatasetName = None
61
- table: TableName = None
62
-
63
- class Response(BaseModel):
64
- table: Table | None
65
-
66
-
67
- class GetTable:
68
- class Request(BaseModel):
69
- id: str
70
-
71
- class Response(BaseModel):
72
- table: Table
73
-
74
-
75
- class ListTables:
76
- class Request(PagedRequest):
77
- project_id: ProjectId
78
- dataset: DatasetName | None = None
79
-
80
- class Response(PagedResponse[Table]): ...
81
-
82
-
83
- class TableService(ServiceBase):
84
- def create(self, req: CreateTable.Request) -> CreateTable.Response:
85
- return self.client.post("/table/create", req, CreateTable.Response)
86
-
87
- def find(self, req: FindTable.Request) -> FindTable.Response:
88
- return self.client.put("/table/find", req, FindTable.Response)
89
-
90
- def get(self, req: GetTable.Request) -> GetTable.Response:
91
- return self.client.put(f"/table/{req.id}", GetTable.Response)
92
-
93
- def list(self, req: ListTables.Request) -> Paged[Table]:
94
- return self.client.paged("/table/list", req, ListTables.Response)
spiral/api/tokens.py DELETED
@@ -1,56 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
- from . import Paged, PagedRequest, PagedResponse, ServiceBase
4
-
5
-
6
- class Token(BaseModel):
7
- id: str
8
- project_id: str
9
- on_behalf_of: str
10
-
11
-
12
- class ExchangeToken:
13
- class Request(BaseModel): ...
14
-
15
- class Response(BaseModel):
16
- token: str
17
-
18
-
19
- class IssueToken:
20
- class Request(BaseModel): ...
21
-
22
- class Response(BaseModel):
23
- token: Token
24
- token_secret: str
25
-
26
-
27
- class RevokeToken:
28
- class Request(BaseModel):
29
- token_id: str
30
-
31
- class Response(BaseModel):
32
- token: Token
33
-
34
-
35
- class ListTokens:
36
- class Request(PagedRequest):
37
- project_id: str
38
- on_behalf_of: str | None = None
39
-
40
- class Response(PagedResponse[Token]): ...
41
-
42
-
43
- class TokenService(ServiceBase):
44
- def exchange(self) -> ExchangeToken.Response:
45
- """Exchange a basic / identity token to a short-lived Spiral token."""
46
- return self.client.post("/token/exchange", ExchangeToken.Request(), ExchangeToken.Response)
47
-
48
- def issue(self) -> IssueToken.Response:
49
- """Issue an API token on behalf of a principal."""
50
- return self.client.post("/token/issue", IssueToken.Request(), IssueToken.Response)
51
-
52
- def revoke(self, request: RevokeToken.Request) -> RevokeToken.Response:
53
- return self.client.put("/token/revoke", request, RevokeToken.Response)
54
-
55
- def list(self, request: ListTokens.Request) -> Paged[Token]:
56
- return self.client.paged("/token/list", request, ListTokens.Response)
spiral/authn/authn.py DELETED
@@ -1,89 +0,0 @@
1
- import base64
2
- import logging
3
- import os
4
-
5
- from spiral.api import Authn, SpiralAPI
6
-
7
- ENV_TOKEN_ID = "SPIRAL_TOKEN_ID"
8
- ENV_TOKEN_SECRET = "SPIRAL_TOKEN_SECRET"
9
-
10
- log = logging.getLogger(__name__)
11
-
12
-
13
- class FallbackAuthn(Authn):
14
- """Credential provider that tries multiple providers in order."""
15
-
16
- def __init__(self, providers: list[Authn]):
17
- self._providers = providers
18
-
19
- def token(self) -> str | None:
20
- for provider in self._providers:
21
- token = provider.token()
22
- if token is not None:
23
- return token
24
- return None
25
-
26
-
27
- class TokenAuthn(Authn):
28
- """Credential provider that returns a fixed token."""
29
-
30
- def __init__(self, token: str):
31
- self._token = token
32
-
33
- def token(self) -> str:
34
- return self._token
35
-
36
-
37
- class EnvironmentAuthn(Authn):
38
- """Credential provider that returns a basic token from the environment.
39
-
40
- NOTE: Returns basic token. Must be exchanged.
41
- """
42
-
43
- def token(self) -> str | None:
44
- if ENV_TOKEN_ID not in os.environ:
45
- return None
46
- if ENV_TOKEN_SECRET not in os.environ:
47
- raise ValueError(f"{ENV_TOKEN_SECRET} is missing.")
48
-
49
- token_id = os.environ[ENV_TOKEN_ID]
50
- token_secret = os.environ[ENV_TOKEN_SECRET]
51
- basic_token = base64.b64encode(f"{token_id}:{token_secret}".encode()).decode("utf-8")
52
-
53
- return basic_token
54
-
55
-
56
- class DeviceAuthProvider(Authn):
57
- """Auth provider that uses the device flow to authenticate a Spiral user."""
58
-
59
- def __init__(self, device_auth):
60
- # NOTE(ngates): device_auth: spiral.auth.device_code.DeviceAuth
61
- # We don't type it to satisfy our import linter
62
- self._device_auth = device_auth
63
-
64
- def token(self) -> str | None:
65
- # TODO(ngates): only run this if we're in a notebook, CLI, or otherwise on the user's machine.
66
- return self._device_auth.authenticate().access_token
67
-
68
-
69
- class TokenExchangeProvider(Authn):
70
- """Auth provider that exchanges a basic token for a Spiral token."""
71
-
72
- def __init__(self, authn: Authn, base_url: str):
73
- self._authn = authn
74
- self._token_service = SpiralAPI(authn, base_url).token
75
-
76
- self._sp_token = None
77
-
78
- def token(self) -> str | None:
79
- if self._sp_token is not None:
80
- return self._sp_token
81
-
82
- # Don't try to exchange if token is not discovered.
83
- if self._authn.token() is None:
84
- return None
85
-
86
- log.debug("Exchanging token")
87
- self._sp_token = self._token_service.exchange().token
88
-
89
- return self._sp_token
spiral/authn/device.py DELETED
@@ -1,206 +0,0 @@
1
- import logging
2
- import sys
3
- import textwrap
4
- import time
5
- import webbrowser
6
- from pathlib import Path
7
-
8
- import httpx
9
- import jwt
10
- from pydantic import BaseModel
11
-
12
- log = logging.getLogger(__name__)
13
-
14
-
15
- class TokensModel(BaseModel):
16
- access_token: str
17
- refresh_token: str
18
-
19
- @property
20
- def organization_id(self) -> str | None:
21
- return self.unverified_access_token().get("org_id")
22
-
23
- def unverified_access_token(self):
24
- return jwt.decode(self.access_token, options={"verify_signature": False})
25
-
26
-
27
- class AuthModel(BaseModel):
28
- tokens: TokensModel | None = None
29
-
30
-
31
- class DeviceAuth:
32
- def __init__(
33
- self,
34
- auth_file: Path,
35
- domain: str,
36
- client_id: str,
37
- http: httpx.Client = None,
38
- ):
39
- self._auth_file = auth_file
40
- self._domain = domain
41
- self._client_id = client_id
42
- self._http = http or httpx.Client()
43
-
44
- if self._auth_file.exists():
45
- with self._auth_file.open("r") as f:
46
- self._auth = AuthModel.model_validate_json(f.read())
47
- else:
48
- self._auth = AuthModel()
49
-
50
- self._default_scope = ["email", "profile"]
51
-
52
- def is_authenticated(self) -> bool:
53
- """Check if the user is authenticated."""
54
- tokens = self._auth.tokens
55
- if tokens is None:
56
- return False
57
-
58
- # Give ourselves a 30-second buffer before the token expires.
59
- return tokens.unverified_access_token()["exp"] - 30 > time.time()
60
-
61
- def authenticate(self, force: bool = False, refresh: bool = False, organization_id: str = None) -> TokensModel:
62
- """Blocking call to authenticate the user.
63
-
64
- Triggers a device code flow and polls for the user to login.
65
- """
66
- if force:
67
- return self._device_code(organization_id)
68
-
69
- if refresh:
70
- if self._auth.tokens is None:
71
- raise ValueError("No tokens to refresh.")
72
- tokens = self._refresh(self._auth.tokens, organization_id)
73
- if not tokens:
74
- raise ValueError("Failed to refresh token.")
75
- return tokens
76
-
77
- # Check for mis-matched organization.
78
- if organization_id is not None:
79
- tokens = self._auth.tokens
80
- if tokens is not None and tokens.unverified_access_token().get("org_id") != organization_id:
81
- tokens = self._refresh(self._auth.tokens, organization_id)
82
- if tokens is None:
83
- return self._device_code(organization_id)
84
-
85
- if self.is_authenticated():
86
- return self._auth.tokens
87
-
88
- # Try to refresh.
89
- tokens = self._auth.tokens
90
- if tokens is not None:
91
- tokens = self._refresh(tokens)
92
- if tokens is not None:
93
- return tokens
94
-
95
- # Otherwise, we kick off the device code flow.
96
- return self._device_code(organization_id)
97
-
98
- def logout(self):
99
- self._remove_tokens()
100
-
101
- def _device_code(self, organization_id: str | None):
102
- scope = " ".join(self._default_scope)
103
- res = self._http.post(
104
- f"{self._domain}/auth/device/code",
105
- data={
106
- "client_id": self._client_id,
107
- "scope": scope,
108
- "organization_id": organization_id,
109
- },
110
- )
111
- res = res.raise_for_status().json()
112
- device_code = res["device_code"]
113
- user_code = res["user_code"]
114
- expires_at = res["expires_in"] + time.time()
115
- interval = res["interval"]
116
- verification_uri_complete = res["verification_uri_complete"]
117
-
118
- # We need to detect if the user is running in a terminal, in Jupyter, etc.
119
- # For now, we'll try to open the browser.
120
- sys.stderr.write(
121
- textwrap.dedent(
122
- f"""
123
- Please login here: {verification_uri_complete}
124
- Your code is {user_code}.
125
- """
126
- )
127
- )
128
-
129
- # Try to open the browser (this also works if the Jupiter notebook is running on the user's machine).
130
- opened = webbrowser.open(verification_uri_complete)
131
-
132
- # If we have a server-side Jupyter notebook, we can try to open with client-side JavaScript.
133
- if not opened and _in_notebook():
134
- from IPython.display import Javascript, display
135
-
136
- display(Javascript(f'window.open("{verification_uri_complete}");'))
137
-
138
- # In the meantime, we need to poll for the user to login.
139
- while True:
140
- if time.time() > expires_at:
141
- raise TimeoutError("Login timed out.")
142
- time.sleep(interval)
143
- res = self._http.post(
144
- f"{self._domain}/auth/token",
145
- data={
146
- "client_id": self._client_id,
147
- "device_code": device_code,
148
- "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
149
- },
150
- )
151
- if not res.is_success:
152
- continue
153
-
154
- tokens = TokensModel(
155
- access_token=res.json()["access_token"],
156
- refresh_token=res.json()["refresh_token"],
157
- )
158
- self._save_tokens(tokens)
159
- return self._auth.tokens
160
-
161
- def _refresh(self, tokens: TokensModel, organization_id: str = None) -> TokensModel | None:
162
- """Attempt to use the refresh token."""
163
- log.debug("Refreshing token %s", self._client_id)
164
-
165
- res = self._http.post(
166
- f"{self._domain}/auth/refresh",
167
- data={
168
- "client_id": self._client_id,
169
- "grant_type": "refresh_token",
170
- "refresh_token": tokens.refresh_token,
171
- "organization_id": organization_id,
172
- },
173
- )
174
- if not res.is_success:
175
- print("Failed to refresh token", res.status_code, res.text)
176
- return None
177
-
178
- tokens = TokensModel(
179
- access_token=res.json()["access_token"],
180
- refresh_token=res.json()["refresh_token"],
181
- )
182
- self._save_tokens(tokens)
183
- return tokens
184
-
185
- def _save_tokens(self, tokens: TokensModel):
186
- self._auth = self._auth.model_copy(update={"tokens": tokens})
187
- self._auth_file.parent.mkdir(parents=True, exist_ok=True)
188
- with self._auth_file.open("w") as f:
189
- f.write(self._auth.model_dump_json(exclude_defaults=True))
190
-
191
- def _remove_tokens(self):
192
- self._auth_file.unlink(missing_ok=True)
193
- self._auth = self._auth.model_copy(update={"tokens": None})
194
-
195
-
196
- def _in_notebook():
197
- try:
198
- from IPython import get_ipython
199
-
200
- if "IPKernelApp" not in get_ipython().config: # pragma: no cover
201
- return False
202
- except ImportError:
203
- return False
204
- except AttributeError:
205
- return False
206
- return True
spiral/authn/github_.py DELETED
@@ -1,33 +0,0 @@
1
- import os
2
-
3
- import httpx
4
-
5
- from spiral.api import Authn
6
-
7
-
8
- class GitHubActionsProvider(Authn):
9
- AUDIENCE = "https://iss.spiraldb.com"
10
-
11
- def __init__(self):
12
- self._gh_token = None
13
-
14
- def token(self) -> str | None:
15
- if self._gh_token is not None:
16
- return self._gh_token
17
-
18
- if os.environ.get("GITHUB_ACTIONS") == "true":
19
- # Next, we check to see if we're running in GitHub actions and if so, grab an ID token.
20
- if "ACTIONS_ID_TOKEN_REQUEST_TOKEN" in os.environ and "ACTIONS_ID_TOKEN_REQUEST_URL" in os.environ:
21
- if not hasattr(self, "__gh_token"):
22
- resp = httpx.get(
23
- f"{os.environ['ACTIONS_ID_TOKEN_REQUEST_URL']}&audience={self.AUDIENCE}",
24
- headers={"Authorization": f'Bearer {os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]}'},
25
- )
26
- if not resp.is_success:
27
- raise ValueError(f"Failed to get GitHub Actions ID token: {resp.text}", resp)
28
- self._gh_token = resp.json()["value"]
29
- else:
30
- raise ValueError("Please set 'id-token: write' permission for this GitHub Actions workflow.")
31
-
32
- # For now, we don't exchange the token for a Spiral one.
33
- return self._gh_token
spiral/authn/modal_.py DELETED
@@ -1,18 +0,0 @@
1
- import os
2
-
3
- from spiral.api import Authn
4
-
5
-
6
- class ModalProvider(Authn):
7
- def __init__(self):
8
- self._modal_token = None
9
-
10
- def token(self) -> str | None:
11
- if self._modal_token is not None:
12
- return self._modal_token
13
-
14
- if os.environ.get("MODAL_IDENTITY_TOKEN") is not None:
15
- self._modal_token = os.environ["MODAL_IDENTITY_TOKEN"]
16
-
17
- # For now, we don't exchange the token for a Spiral one.
18
- return self._modal_token
spiral/cli/org.py DELETED
@@ -1,90 +0,0 @@
1
- import webbrowser
2
- from typing import Annotated
3
-
4
- import jwt
5
- import rich
6
- import typer
7
- from rich.table import Table
8
- from typer import Option
9
-
10
- from spiral.api.organizations import CreateOrganization, InviteUser, OrganizationRole, PortalLink
11
- from spiral.cli import AsyncTyper, OptionalStr, state
12
- from spiral.cli.types import OrganizationArg
13
-
14
- app = AsyncTyper()
15
-
16
-
17
- @app.command(help="Switch the active organization.")
18
- def switch(org_id: OrganizationArg):
19
- state.settings.spiraldb.device_auth().authenticate(refresh=True, organization_id=org_id)
20
- rich.print(f"Switched to organization: {org_id}")
21
-
22
-
23
- @app.command(help="Create a new organization.")
24
- def create(
25
- name: Annotated[OptionalStr, Option(help="The human-readable name of the organization.")] = None,
26
- ):
27
- res = state.settings.api.organization.create_organization(CreateOrganization.Request(name=name))
28
-
29
- # Authenticate to the new organization
30
- state.settings.spiraldb.device_auth().authenticate(refresh=True, organization_id=res.organization.id)
31
-
32
- rich.print(f"{res.organization.name} [dim]{res.organization.id}[/dim]")
33
-
34
-
35
- @app.command(help="List organizations.")
36
- def ls():
37
- org_id = current_org_id()
38
-
39
- table = Table("", "id", "name", "role", title="Organizations")
40
- for m in state.settings.api.organization.list_user_memberships():
41
- table.add_row("👉" if m.organization.id == org_id else "", m.organization.id, m.organization.name, m.role)
42
-
43
- rich.print(table)
44
-
45
-
46
- @app.command(help="Invite a user to the organization.")
47
- def invite(email: str, role: OrganizationRole = "member", expires_in_days: int = 7):
48
- state.settings.api.organization.invite_user(
49
- InviteUser.Request(email=email, role=role, expires_in_days=expires_in_days)
50
- )
51
- rich.print(f"Invited {email} as a {role.value}.")
52
-
53
-
54
- @app.command(help="Configure single sign-on for your organization.")
55
- def sso():
56
- _do_action(PortalLink.Intent.SSO)
57
-
58
-
59
- @app.command(help="Configure directory services for your organization.")
60
- def directory():
61
- _do_action(PortalLink.Intent.DIRECTORY)
62
-
63
-
64
- @app.command(help="Configure audit logs for your organization.")
65
- def audit_logs():
66
- _do_action(PortalLink.Intent.AUDIT_LOGS)
67
-
68
-
69
- @app.command(help="Configure log streams for your organization.")
70
- def log_streams():
71
- _do_action(PortalLink.Intent.LOG_STREAMS)
72
-
73
-
74
- @app.command(help="Configure domains for your organization.")
75
- def domains():
76
- _do_action(PortalLink.Intent.DOMAIN_VERIFICATION)
77
-
78
-
79
- def _do_action(intent: PortalLink.Intent):
80
- res = state.settings.api.organization.portal_link(PortalLink.Request(intent=intent))
81
- rich.print(f"Opening the configuration portal:\n{res.url}")
82
- webbrowser.open(res.url)
83
-
84
-
85
- def current_org_id():
86
- org_id = jwt.decode(state.settings.authn.token(), options={"verify_signature": False}).get("org_id")
87
- if not org_id:
88
- rich.print("[red]You are not logged in to an organization.[/red]")
89
- raise typer.Exit(1)
90
- return org_id