Skip to content

Commit 48790bb

Browse files
authored
Merge pull request #56 from edanalytics/feature/lightbeam_create
implementation of new `lightbeam create` functionality
2 parents d22170e + 0379182 commit 48790bb

File tree

4 files changed

+133
-0
lines changed

4 files changed

+133
-0
lines changed

lightbeam/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def emit(self, record):
2323
"truncate": "truncate",
2424
"count": "count",
2525
"fetch": "fetch",
26+
"create": "create"
2627
}
2728
command_list = ', '.join(f"'{c}'" for c in ALLOWED_COMMANDS.values())
2829

@@ -175,6 +176,7 @@ def main(argv=None):
175176
try:
176177
logger.info("starting...")
177178
if args.command==ALLOWED_COMMANDS['count']: lb.counter.count()
179+
if args.command==ALLOWED_COMMANDS['create']: lb.creator.create()
178180
elif args.command==ALLOWED_COMMANDS['fetch']: lb.fetcher.fetch()
179181
elif args.command==ALLOWED_COMMANDS['validate']: lb.validator.validate()
180182
elif args.command==ALLOWED_COMMANDS['send']: lb.sender.send()

lightbeam/api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ def get_params_for_endpoint(self, endpoint, type='required'):
346346
definition = util.get_swagger_ref_for_endpoint(self.lightbeam.config["namespace"], swagger, endpoint)
347347
if type=='required':
348348
return self.get_required_params_from_swagger(swagger, definition)
349+
elif type=='all':
350+
return self.get_all_params_from_swagger(swagger, definition)
349351
else:
350352
# descriptor endpoints all have the same structure and identity fields:
351353
if "Descriptor" in endpoint:
@@ -369,6 +371,28 @@ def get_required_params_from_swagger(self, swagger, definition, prefix=""):
369371
params[prop] = prefix + prop
370372
return params
371373

374+
def get_all_params_from_swagger(self, swagger, definition, prefix=""):
375+
params = {}
376+
schema = util.resolve_swagger_ref(swagger, definition)
377+
if not schema:
378+
self.logger.critical(f"Swagger contains neither `definitions` nor `components.schemas` - check that the Swagger is valid.")
379+
380+
for prop in schema["properties"].keys():
381+
if prop in ["_etag", "id", "link"]: continue
382+
if "required" in schema.keys() and prop in schema["required"]: prop_name = "[required]"+prop
383+
else: prop_name = "[optional]"+prop
384+
if "$ref" in schema["properties"][prop].keys():
385+
params[prop_name] = {}
386+
sub_definition = schema["properties"][prop]["$ref"]
387+
sub_params = self.get_all_params_from_swagger(swagger, sub_definition, prefix=prefix+prop+"_")
388+
for k,v in sub_params.items():
389+
params[prop_name][k] = v
390+
elif schema["properties"][prop]["type"]!="array":
391+
params[prop_name] = f"[{schema['properties'][prop]['type']}]" + prefix + prop
392+
else:
393+
params[prop_name] = [self.get_all_params_from_swagger(swagger, schema["properties"][prop]["items"]["$ref"], prefix=prefix+prop+"-")]
394+
return params
395+
372396
def get_identity_params_from_swagger(self, swagger, definition, prefix=""):
373397
params = {}
374398
schema = util.resolve_swagger_ref(swagger, definition)

lightbeam/create.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import re
3+
import json
4+
import yaml
5+
from lightbeam import util
6+
7+
class Creator:
8+
9+
def __init__(self, lightbeam=None):
10+
self.lightbeam = lightbeam
11+
self.logger = self.lightbeam.logger
12+
self.template_folder = "templates/"
13+
self.earthmover_file = "earthmover.yml"
14+
15+
def create(self):
16+
self.lightbeam.api.load_swagger_docs()
17+
os.makedirs(self.template_folder, exist_ok=True)
18+
earthmover_yaml = {}
19+
# check if file exists!
20+
if os.path.isfile(self.earthmover_file):
21+
with open(self.earthmover_file) as file:
22+
earthmover_yaml = yaml.safe_load(file)
23+
for endpoint in self.lightbeam.endpoints:
24+
if endpoint in (earthmover_yaml.get("destinations", {}) or {}).keys():
25+
self.logger.critical(f"The file `{self.earthmover_file}` already exists in the current directory and contains `$destinations.{endpoint}`; to re-create it, please first manually remove it (and `{self.template_folder}{endpoint}.jsont`).")
26+
self.create_jsont(endpoint)
27+
# write out earthmover_yaml
28+
if not os.path.isfile(self.earthmover_file):
29+
self.logger.info(f"creating file `{self.earthmover_file}`...")
30+
with open(self.earthmover_file, 'w+') as file:
31+
file.write("""version: 2.0
32+
33+
# This is an earthmover.yml file, generated with `lightbeam create`, for creating Ed-Fi JSON payloads
34+
# using earthmover. See https://github.com/edanalytics/earthmover for documentation.
35+
36+
config:
37+
macros: >
38+
{% macro descriptor_namespace() -%}
39+
uri://ed-fi.org
40+
{%- endmacro %}
41+
42+
# Define your source data here:
43+
sources:
44+
# Example:
45+
# mysource:
46+
# file: path/to/myfile.csv
47+
# ...
48+
49+
# (If needed, define your data transformations here:)
50+
# transformations:
51+
52+
destinations:""")
53+
for endpoint in self.lightbeam.endpoints:
54+
file.write(self.create_em_destination_node(endpoint))
55+
else:
56+
self.logger.info(f"appending to file `{self.earthmover_file}`...")
57+
with open(self.earthmover_file, 'a+') as file:
58+
for endpoint in self.lightbeam.endpoints:
59+
file.write(self.create_em_destination_node(endpoint))
60+
61+
def create_em_destination_node(self, endpoint):
62+
return f"""
63+
{endpoint}:
64+
source: $transformations.{endpoint}
65+
template: {self.template_folder}{endpoint}.jsont
66+
extension: jsonl
67+
linearize: True"""
68+
69+
def upper_repl(self, match):
70+
value = match.group(1)
71+
return "/" + value[0].upper() + value[1:] + "Descriptor#"
72+
73+
74+
def create_jsont(self, endpoint):
75+
template_file = f"{self.template_folder}{endpoint}.jsont"
76+
# check if file exists!
77+
if os.path.isfile(template_file):
78+
self.logger.critical(f"The file `{template_file}` already exists in the current directory; to re-create it, please first manually delete it.")
79+
# generate base JSON structure:
80+
content = self.lightbeam.api.get_params_for_endpoint(endpoint, type='all')
81+
# pretty-print it:
82+
content = json.dumps(content, indent=2)
83+
# annotate required/optional properties:
84+
content = content.replace('"[required]', '{# (required) #} "')
85+
content = content.replace('"[optional]', '{# (optional) #} "')
86+
# appropriate quoting based on property data type:
87+
content = re.sub('"\[string\](.*)Descriptor"', r'"{{descriptor_namespace()}}/\1Descriptor#{{\1Descriptor}}"', content)
88+
content = re.sub('/(.*)_(.*)Descriptor#', r'/\2Descriptor#', content)
89+
content = re.sub(r'/(.*)Descriptor#', self.upper_repl, content)
90+
content = re.sub('"\[string\](.*)"', r'"{{\1}}"', content)
91+
content = re.sub('"\[(integer|boolean)\](.*)"', r'{{\2}}', content)
92+
# for loops over arrays:
93+
content = re.sub('"(.*)": \[', r'"\1": [ {% for item in \1 %}', content)
94+
content = re.sub('\]', r'{% endfor %} ]', content)
95+
content = re.sub('{{(.*)-(.*)}}', r'{{item.\2}}', content)
96+
# add info header message:
97+
content = """{#
98+
This is an earthmover JSON template file, generated with `lightbeam create`, for creating Ed-Fi JSON `"""+endpoint+"""`
99+
payloads using earthmover. See https://github.com/edanalytics/earthmover for documentation.
100+
#}
101+
""" + content
102+
# write out json template
103+
self.logger.info(f"creating file `{template_file}`...")
104+
with open(template_file, 'w+') as file:
105+
file.write(content)

lightbeam/lightbeam.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from lightbeam import util
1212
from lightbeam.api import EdFiAPI
1313
from lightbeam.count import Counter
14+
from lightbeam.create import Creator
1415
from lightbeam.fetch import Fetcher
1516
from lightbeam.validate import Validator
1617
from lightbeam.send import Sender
@@ -73,6 +74,7 @@ def __init__(self, config_file, logger=None, selector="*", exclude="", keep_keys
7374
self.endpoints = []
7475
self.results = []
7576
self.counter = Counter(self)
77+
self.creator = Creator(self)
7678
self.fetcher = Fetcher(self)
7779
self.validator = Validator(self)
7880
self.sender = Sender(self)

0 commit comments

Comments
 (0)