jvcli 2.0.9__py3-none-any.whl → 2.0.11__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.
jvcli/__init__.py CHANGED
@@ -4,5 +4,5 @@ jvcli package initialization.
4
4
  This package provides the CLI tool for Jivas Package Repository.
5
5
  """
6
6
 
7
- __version__ = "2.0.9"
7
+ __version__ = "2.0.11"
8
8
  __supported__jivas__versions__ = ["2.0.0"]
jvcli/api.py CHANGED
@@ -62,7 +62,7 @@ class RegistryAPI:
62
62
  token: Optional[str] = None,
63
63
  api_key: Optional[str] = None,
64
64
  ) -> dict:
65
- """Get action info.yaml content as json"""
65
+ """Get package info.yaml content as json"""
66
66
  endpoint = "info"
67
67
 
68
68
  try:
@@ -83,12 +83,12 @@ class RegistryAPI:
83
83
  return response.json()
84
84
  else:
85
85
  click.secho(
86
- f"Error retrieving action: {response.json()['error']}",
86
+ f"Error retrieving package: {response.json()['error']}",
87
87
  fg="red",
88
88
  )
89
89
  return {}
90
90
  except Exception as e:
91
- click.secho(f"Error retrieving action: {e}", fg="red")
91
+ click.secho(f"Error retrieving package: {e}", fg="red")
92
92
  return {}
93
93
 
94
94
  @staticmethod
@@ -133,42 +133,6 @@ class RegistryAPI:
133
133
  click.secho(f"Error downloading package: {e}", fg="red")
134
134
  return {}
135
135
 
136
- @staticmethod
137
- def get_action_info(
138
- name: str,
139
- version: str = "",
140
- token: Optional[str] = None,
141
- api_key: Optional[str] = None,
142
- ) -> dict:
143
- """Get action info.yaml content as json"""
144
- endpoint = "info"
145
-
146
- try:
147
- headers = {"Authorization": f"Bearer {token}"} if token else {}
148
-
149
- if api_key:
150
- headers["x-api-key"] = api_key
151
-
152
- data = {
153
- "name": name,
154
- "version": "" if version == "latest" else version,
155
- }
156
- response = requests.get(
157
- RegistryAPI.url + endpoint, params=data, headers=headers
158
- )
159
- # Check if the response is successful
160
- if response.status_code == 200:
161
- return response.json()
162
- else:
163
- click.secho(
164
- f"Error retrieving action: {response.json()['error']}",
165
- fg="red",
166
- )
167
- return {}
168
- except Exception as e:
169
- click.secho(f"Error retrieving action: {e}", fg="red")
170
- return {}
171
-
172
136
  @staticmethod
173
137
  def create_namespace(name: str, token: str) -> dict:
174
138
  """Create a namespace."""
@@ -181,7 +181,7 @@ def dynamic_form(
181
181
  {"id": 0, "fields": {field["name"]: "" for field in field_definitions}}
182
182
  ]
183
183
 
184
- def add_row() -> None:
184
+ def add_row() -> None: # pragma: no cover, don't know how to test this yet 😅
185
185
  """Add a new row to the dynamic form."""
186
186
  new_id = (
187
187
  max((item["id"] for item in st.session_state[session_key]), default=-1) + 1
@@ -192,7 +192,9 @@ def dynamic_form(
192
192
  }
193
193
  st.session_state[session_key].append(new_row)
194
194
 
195
- def remove_row(id_to_remove: int) -> None:
195
+ def remove_row(
196
+ id_to_remove: int,
197
+ ) -> None: # pragma: no cover, don't know how to test this yet 😅
196
198
  """Remove a row from the dynamic form."""
197
199
  st.session_state[session_key] = [
198
200
  item for item in st.session_state[session_key] if item["id"] != id_to_remove
jvcli/commands/auth.py CHANGED
@@ -26,14 +26,14 @@ def signup(username: str, email: str, password: str) -> None:
26
26
 
27
27
  @click.command()
28
28
  @click.option(
29
- "--login",
29
+ "--username",
30
30
  help="Your email address or username.",
31
31
  prompt="Login (username or email)",
32
32
  )
33
33
  @click.option("--password", prompt=True, hide_input=True, help="Your password.")
34
- def login(login: str, password: str) -> None:
34
+ def login(username: str, password: str) -> None:
35
35
  """Log in to your Jivas Package Repository account."""
36
- data = RegistryAPI.login(login, password)
36
+ data = RegistryAPI.login(username, password)
37
37
  if data and "token" in data and "namespaces" in data and "email" in data:
38
38
  save_token(data["token"], data["namespaces"], data["email"])
39
39
  click.secho("Login successful! Token saved.", fg="green", bold=True)
jvcli/commands/studio.py CHANGED
@@ -2,39 +2,44 @@
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
+ from typing import Annotated
5
6
 
6
7
  import click
7
8
  import jaclang # noqa: F401
8
9
  from bson import ObjectId
10
+ from fastapi import Depends, FastAPI, HTTPException
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import JSONResponse
13
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
14
+ from fastapi.staticfiles import StaticFiles
9
15
  from jac_cloud.core.architype import NodeAnchor
16
+ from jac_cloud.jaseci.security import decrypt
10
17
  from uvicorn import run
11
18
 
12
19
 
13
- @click.group()
14
- def studio() -> None:
15
- """Group for managing Jivas Studio resources."""
16
- pass # pragma: no cover
20
+ def get_nodes_and_edges(
21
+ nid: str,
22
+ current_depth: int,
23
+ depth: int,
24
+ nodes: list,
25
+ edges: list,
26
+ node_collection: NodeAnchor.Collection,
27
+ edge_collection: NodeAnchor.Collection,
28
+ ) -> None:
29
+ """Get nodes and edges recursively."""
30
+ if current_depth >= depth:
31
+ return
17
32
 
33
+ outgoing_edges = edge_collection.find(
34
+ {
35
+ "$or": [
36
+ {"source": nid},
37
+ {"source": {"$regex": f"{nid}$"}},
38
+ ]
39
+ }
40
+ )
18
41
 
19
- def get_graph(root: str) -> dict:
20
- """Fetches a graph structure from the database."""
21
- nodes = []
22
- edges = []
23
-
24
- edge_collection = NodeAnchor.Collection.get_collection("edge")
25
- node_collection = NodeAnchor.Collection.get_collection("node")
26
- node_docs = node_collection.find({"root": ObjectId(root)})
27
- edge_docs = edge_collection.find({"root": ObjectId(root)})
28
-
29
- for node in node_docs:
30
- nodes.append(
31
- {
32
- "id": node["_id"],
33
- "data": node["architype"],
34
- "name": node["name"],
35
- }
36
- )
37
- for edge in edge_docs:
42
+ for edge in outgoing_edges:
38
43
  edges.append(
39
44
  {
40
45
  "id": edge["_id"],
@@ -45,40 +50,181 @@ def get_graph(root: str) -> dict:
45
50
  }
46
51
  )
47
52
 
48
- return {
49
- "nodes": json.loads(json.dumps(nodes, default=str)),
50
- "edges": json.loads(json.dumps(edges, default=str)),
51
- }
53
+ node_id = edge["target"].split(":")[-1]
54
+ connected_nodes = node_collection.find({"_id": ObjectId(node_id)})
52
55
 
56
+ for node in connected_nodes:
57
+ nodes.append(
58
+ {
59
+ "id": node["_id"],
60
+ "data": node["architype"],
61
+ "name": node["name"],
62
+ }
63
+ )
53
64
 
54
- def get_users() -> list:
55
- """Fetches users from the database."""
56
- users = []
57
- user_collection = NodeAnchor.Collection.get_collection("user")
58
- user_docs = user_collection.find()
65
+ get_nodes_and_edges(
66
+ node["_id"],
67
+ current_depth + 1,
68
+ depth,
69
+ nodes,
70
+ edges,
71
+ node_collection,
72
+ edge_collection,
73
+ )
59
74
 
60
- for user in user_docs:
61
- users.append(
62
- {
63
- "id": user["_id"],
64
- "root_id": user["root_id"],
65
- "email": user["email"],
66
- }
67
- )
68
75
 
69
- return json.loads(json.dumps(users, default=str))
76
+ # need this because endpoint annotation differs depending on require auth flag
77
+ class EndpointFactory:
78
+ """Factory for creating endpoints based on require_auth flag."""
79
+
80
+ @staticmethod
81
+ def create_endpoints(require_auth: bool, security: HTTPBearer | None) -> tuple:
82
+ """Create endpoints based on require_auth flag."""
83
+
84
+ def validate_auth(credentials: HTTPAuthorizationCredentials) -> None:
85
+ """Validate authentication token."""
86
+ token = credentials.credentials
87
+ if not token or not decrypt(token):
88
+ raise HTTPException(status_code=401, detail="Invalid token")
89
+
90
+ def get_graph_data(root: str) -> dict:
91
+ """Get graph nodes and edges data."""
92
+ edge_collection = NodeAnchor.Collection.get_collection("edge")
93
+ node_collection = NodeAnchor.Collection.get_collection("node")
94
+
95
+ nodes = [
96
+ {
97
+ "id": node["_id"],
98
+ "data": node["architype"],
99
+ "name": node["name"],
100
+ }
101
+ for node in node_collection.find({"root": ObjectId(root)})
102
+ ]
103
+
104
+ edges = [
105
+ {
106
+ "id": edge["_id"],
107
+ "name": edge["name"],
108
+ "source": edge["source"],
109
+ "target": edge["target"],
110
+ "data": edge["architype"],
111
+ }
112
+ for edge in edge_collection.find({"root": ObjectId(root)})
113
+ ]
114
+
115
+ return {"nodes": nodes, "edges": edges}
116
+
117
+ def get_users_data() -> list:
118
+ """Get users data."""
119
+ user_collection = NodeAnchor.Collection.get_collection("user")
120
+ return [
121
+ {
122
+ "id": user["_id"],
123
+ "root_id": user["root_id"],
124
+ "email": user["email"],
125
+ }
126
+ for user in user_collection.find()
127
+ ]
128
+
129
+ def get_node_connections(node_id: str, depth: int) -> dict:
130
+ nid = node_id.split(":")[-1]
131
+ current_depth = 0
132
+ nodes: list = []
133
+ edges: list = []
134
+
135
+ edge_collection = NodeAnchor.Collection.get_collection("edge")
136
+ node_collection = NodeAnchor.Collection.get_collection("node")
137
+
138
+ get_nodes_and_edges(
139
+ nid,
140
+ current_depth,
141
+ depth,
142
+ nodes,
143
+ edges,
144
+ node_collection,
145
+ edge_collection,
146
+ )
147
+
148
+ return {"nodes": nodes, "edges": edges}
149
+
150
+ if not require_auth:
151
+
152
+ async def graph_endpoint(root: str) -> JSONResponse:
153
+ return JSONResponse(
154
+ content=json.loads(json.dumps(get_graph_data(root), default=str))
155
+ )
156
+
157
+ async def users_endpoint() -> JSONResponse:
158
+ return JSONResponse(
159
+ content=json.loads(json.dumps(get_users_data(), default=str))
160
+ )
161
+
162
+ async def node_endpoint(node_id: str, depth: int) -> JSONResponse:
163
+ return JSONResponse(
164
+ content=json.loads(
165
+ json.dumps(get_node_connections(node_id, depth), default=str)
166
+ )
167
+ )
168
+
169
+ return graph_endpoint, users_endpoint, node_endpoint
170
+
171
+ else:
172
+
173
+ async def guarded_graph_endpoint(
174
+ root: str,
175
+ credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
176
+ ) -> JSONResponse:
177
+ validate_auth(credentials)
178
+ return JSONResponse(
179
+ content=json.loads(json.dumps(get_graph_data(root), default=str))
180
+ )
181
+
182
+ async def guarded_users_endpoint(
183
+ credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
184
+ ) -> JSONResponse:
185
+ validate_auth(credentials)
186
+ return JSONResponse(
187
+ content=json.loads(json.dumps(get_users_data(), default=str))
188
+ )
189
+
190
+ async def guarded_node_endpoint(
191
+ node_id: str,
192
+ depth: int,
193
+ credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
194
+ ) -> JSONResponse:
195
+ validate_auth(credentials)
196
+ return JSONResponse(
197
+ content=json.loads(
198
+ json.dumps(get_node_connections(node_id, depth), default=str)
199
+ )
200
+ )
201
+
202
+ return guarded_graph_endpoint, guarded_users_endpoint, guarded_node_endpoint
203
+
204
+
205
+ @click.group()
206
+ def studio() -> None:
207
+ """Group for managing Jivas Studio resources."""
208
+ pass # pragma: no cover
70
209
 
71
210
 
72
211
  @studio.command()
73
212
  @click.option("--port", default=8989, help="Port for the studio to launch on.")
74
- def launch(port: int) -> None:
213
+ @click.option(
214
+ "--require-auth", default=False, help="Require authentication for studio api."
215
+ )
216
+ def launch(port: int, require_auth: bool) -> None:
75
217
  """Launch the Jivas Studio on the specified port."""
76
218
  click.echo(f"Launching Jivas Studio on port {port}...")
77
- from fastapi import FastAPI
78
- from fastapi.middleware.cors import CORSMiddleware
79
- from fastapi.staticfiles import StaticFiles
80
219
 
81
- app = FastAPI()
220
+ security = HTTPBearer() if require_auth else None
221
+
222
+ get_graph, get_users, get_node = EndpointFactory.create_endpoints(
223
+ require_auth, security
224
+ )
225
+
226
+ app = FastAPI(title="Jivas Studio API")
227
+
82
228
  app.add_middleware(
83
229
  CORSMiddleware,
84
230
  allow_origins=["*"],
@@ -89,8 +235,24 @@ def launch(port: int) -> None:
89
235
 
90
236
  app.add_api_route("/graph", endpoint=get_graph, methods=["GET"])
91
237
  app.add_api_route("/users", endpoint=get_users, methods=["GET"])
238
+ app.add_api_route("/graph/node", endpoint=get_node, methods=["GET"])
239
+
240
+ client_dir = (
241
+ Path(__file__)
242
+ .resolve()
243
+ .parent.parent.joinpath("studio-auth" if require_auth else "studio")
244
+ )
92
245
 
93
- client_dir = Path(__file__).resolve().parent.parent.joinpath("studio")
94
- app.mount("/", StaticFiles(directory=client_dir, html=True), name="studio")
246
+ app.mount(
247
+ "/",
248
+ StaticFiles(directory=client_dir, html=True),
249
+ name="studio",
250
+ )
251
+
252
+ app.mount(
253
+ "/graph",
254
+ StaticFiles(directory=client_dir, html=True),
255
+ name="studio_graph",
256
+ )
95
257
 
96
258
  run(app, host="0.0.0.0", port=port)