Skip to content

Commit bb74b05

Browse files
authored
Merge pull request #87 from edanalytics/feature/namespace_overrides
adding `namespace_overrides` feature
2 parents 8bd691e + 6c56fc3 commit bb74b05

File tree

13 files changed

+52
-12
lines changed

13 files changed

+52
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### v0.1.11
2+
<details>
3+
<summary>Released 2026-02-17</summary>
4+
* feature: `namespace_overrides` to allow sending resources to different namespaces in one `lightbeam send`
5+
</details>
6+
17
### v0.1.10
28
<details>
39
<summary>Released 2025-11-21</summary>

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ An example YAML configuration is below, followed by documentation of each option
4141
state_dir: ~/.lightbeam/
4242
data_dir: ./
4343
namespace: ed-fi
44+
namespace_overrides:
45+
tpdm:
46+
- candidates
4447
edfi_api:
4548
base_url: https://api.schooldistrict.org/v5.3/api
4649
oauth_url: https://api.schooldistrict.org/v5.3/api/oauth/token
@@ -84,6 +87,7 @@ show_stacktrace: True
8487
* (optional) `state_dir` is where [state](#state) is stored. The default is `~/.lightbeam/` on *nix systems, `C:/Users/USER/.lightbeam/` on Windows systems.
8588
* (optional) Specify the `data_dir` which contains JSONL files to send to Ed-Fi. The default is `./`. The tool will look for files like `{Resource}.jsonl` or `{Descriptor}.jsonl` in this location, as well as directory-based files like `{Resource}/*.jsonl` or `{Descriptor}/*.jsonl`. Files with `.ndjson` or simply `.json` extensions will also be processed. (More info at the [`ndjson` standard page](http://dataprotocols.org/ndjson/).)
8689
* (optional) Specify the `namespace` to use when accessing the Ed-Fi API. The default is `ed-fi` but others include `tpdm` or custom values. To send data to multiple namespaces, you must use a YAML configuration file and `lightbeam send` for each.
90+
* (optional) Specify `namespace_overrides`: a structure where keys are alternate namespaces (beside the above `namespace`) and values are lists of endpoint names that correspond to that namespace. This enables lightbeam to map data files for different endpoints to different namespaces, so you can (for example) transmit `candidates.jsonl` to the `tpdm` namespace and `staffs.jsonl` to the `ed-fi` namespace in a single `lightbeam send`.
8791
* Specify the details of the `edfi_api` to which to connect including
8892
* (optional) The `base_url` which serves a JSON object specifying the paths to data endpoints, Swagger, and dependencies. The default is `https://localhost/api` (the address of an Ed-Fi API [running locally in Docker](https://docs.ed-fi.org/reference/docker/)), but the location varies depending on how Ed-Fi is deployed.
8993
* Most Ed-Fi APIs publish other endpoints they provide in a JSON document at the base URL - `lighbteam` attempts to discover these. If, however, your API does not publish these URLs at the base, or you want to manually override any of them for any reason, you can specify the following:

example/lightbeam.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# state_dir: ~/.lightbeam/
22
data_dir: ./
33
namespace: ed-fi
4+
namespace_overrides:
5+
tpdm:
6+
- candidates
47
edfi_api:
58
# base_url: https://api.ed-fi.org/v5.3/api
69
base_url: https://localhost/api

lightbeam/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.10
1+
0.1.11

lightbeam/api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,15 @@ def get_sorted_endpoints(self):
179179
data = sorted(data, key=lambda x: x['order'])
180180

181181
ordered_endpoints = []
182+
possible_namespaces = [self.lightbeam.config["namespace"]]
183+
if "namespace_overrides" in self.lightbeam.config.keys():
184+
for namespace in self.lightbeam.config["namespace_overrides"].keys():
185+
if namespace == "__line__": continue # (remove YAML parsing artifact)
186+
possible_namespaces.append(namespace)
182187
for e in data:
183-
if e["resource"].startswith("/" + self.lightbeam.config["namespace"] + "/"):
184-
ordered_endpoints.append(e["resource"].replace('/' + self.lightbeam.config["namespace"] + '/', ""))
188+
for namespace in possible_namespaces:
189+
if e["resource"].startswith(f"/{namespace}/"):
190+
ordered_endpoints.append(e["resource"].replace(f"/{namespace}/", ""))
185191
return ordered_endpoints
186192

187193
# Loads the Swagger JSON from the Ed-Fi API
@@ -346,7 +352,7 @@ async def load_descriptors_values(self):
346352
def get_params_for_endpoint(self, endpoint, type='required'):
347353
if "Descriptor" in endpoint: swagger = self.descriptors_swagger
348354
else: swagger = self.resources_swagger
349-
definition = util.get_swagger_ref_for_endpoint(self.lightbeam.config["namespace"], swagger, endpoint)
355+
definition = util.get_swagger_ref_for_endpoint(self.lightbeam.get_namespace_for_endpoint(endpoint), swagger, endpoint)
350356
if type=='required':
351357
return self.get_required_params_from_swagger(swagger, definition)
352358
elif type=='all':

lightbeam/count.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async def get_record_count(self, endpoint, params={}):
5555
# since `count` should finish _very_ quickly - way before expiry
5656
params.update({ "limit": "0", "totalCount": "true" })
5757
async with self.lightbeam.api.client.get(
58-
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.config["namespace"], endpoint),
58+
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.get_namespace_for_endpoint(endpoint), endpoint),
5959
params=params,
6060
ssl=self.lightbeam.config["connection"]["verify_ssl"],
6161
headers=self.lightbeam.api.headers

lightbeam/delete.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def do_delete(self, endpoint, file_name, params, line, data_hash=None):
141141

142142
# we have to get the `id` for a particular resource by first searching for its natural keys
143143
async with self.lightbeam.api.client.get(
144-
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.config["namespace"], endpoint),
144+
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.get_namespace_for_endpoint(endpoint), endpoint),
145145
params=params,
146146
ssl=self.lightbeam.config["connection"]["verify_ssl"],
147147
headers=self.lightbeam.api.headers
@@ -192,7 +192,7 @@ async def do_delete_id(self, endpoint, id, file_name=None, line=None, data_hash=
192192
while True: # this is not great practice, but an effective way (along with the `break` below) to achieve a do:while loop
193193
try:
194194
async with self.lightbeam.api.client.delete(
195-
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.config["namespace"], endpoint, id),
195+
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.get_namespace_for_endpoint(endpoint), endpoint, id),
196196
ssl=self.lightbeam.config["connection"]["verify_ssl"],
197197
headers=self.lightbeam.api.headers
198198
) as delete_response:

lightbeam/fetch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def get_records(self, do_write=True, log_status_counts=True):
3131
if self.lightbeam.query != '':
3232
self.lightbeam.api.load_swagger_docs()
3333
swagger = self.lightbeam.api.resources_swagger
34-
namespace = self.lightbeam.config["namespace"]
34+
namespace = self.lightbeam.get_namespace_for_endpoint(endpoint)
3535
supported_params = swagger.get("paths", {}).get(f"/{namespace}/{endpoint}", {}).get("get", {}).get("parameters", [])
3636
supported_param_names = [ x["name"] for x in supported_params if "name" in x.keys() and "in" in x.keys() and x["in"]=="query" ]
3737
if not set(params.keys()).issubset(set(supported_param_names)):
@@ -76,7 +76,7 @@ async def get_endpoint_records(self, endpoint, limit, offset, file_handle=None):
7676

7777
# send GET request
7878
async with self.lightbeam.api.client.get(
79-
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.config["namespace"], endpoint),
79+
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.get_namespace_for_endpoint(endpoint), endpoint),
8080
params=urlencode(params),
8181
ssl=self.lightbeam.config["connection"]["verify_ssl"],
8282
headers=self.lightbeam.api.headers

lightbeam/lightbeam.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,16 @@ def __init__(self, config_file, logger=None, selector="*", exclude="", keep_keys
125125
os.mkdir(self.config["state_dir"])
126126

127127
# Initialize a dictionary for tracking run metadata (for structured output)
128+
namespace_overrides = self.config["namespace_overrides"] if "namespace_overrides" in self.config.keys() else None
129+
namespace_overrides.pop("__line__") # (remove YAML parsing artifact)
128130
self.metadata = {
129131
"started_at": self.start_timestamp.isoformat(timespec='microseconds'),
130132
"working_dir": os.getcwd(),
131133
"config_file": self.config_file,
132134
"data_dir": self.config["data_dir"],
133135
"api_url": self.config["edfi_api"]["base_url"],
134136
"namespace": self.config["namespace"],
137+
"namespace_overrides": namespace_overrides,
135138
"resources": {}
136139
}
137140

@@ -250,6 +253,16 @@ def confirm_delete(self, endpoints):
250253
def confirm_truncate(self, endpoints):
251254
self._confirm_delete_op(endpoints, "TRUNCATE ALL DATA")
252255

256+
def get_namespace_for_endpoint(self, endpoint):
257+
if "namespace_overrides" in self.config.keys():
258+
for namespace in self.config["namespace_overrides"].keys():
259+
if (
260+
isinstance(self.config["namespace_overrides"][namespace], list)
261+
and endpoint in self.config["namespace_overrides"][namespace]
262+
):
263+
return namespace
264+
return self.config["namespace"]
265+
253266
################### Data discovery and loading methods ####################
254267

255268
# For the specified endpoint, returns a list of all files in config.data_dir which end in .jsonl

lightbeam/send.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ async def do_post(self, endpoint, file_name, data, line_number, data_hash):
142142
while True: # this is not great practice, but an effective way (along with the `break` below) to achieve a do:while loop
143143
try:
144144
async with self.lightbeam.api.client.post(
145-
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.config["namespace"], endpoint),
145+
util.url_join(self.lightbeam.api.config["data_url"], self.lightbeam.get_namespace_for_endpoint(endpoint), endpoint),
146146
data=data,
147147
ssl=self.lightbeam.config["connection"]["verify_ssl"],
148148
headers=self.lightbeam.api.headers

0 commit comments

Comments
 (0)