Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implementation of new lightbeam create functionality #56

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lightbeam/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def emit(self, record):
"truncate": "truncate",
"count": "count",
"fetch": "fetch",
"create": "create"
}
command_list = ', '.join(f"'{c}'" for c in ALLOWED_COMMANDS.values())

Expand Down Expand Up @@ -163,6 +164,7 @@ def main(argv=None):
try:
logger.info("starting...")
if args.command==ALLOWED_COMMANDS['count']: lb.counter.count()
if args.command==ALLOWED_COMMANDS['create']: lb.creator.create()
elif args.command==ALLOWED_COMMANDS['fetch']: lb.fetcher.fetch()
elif args.command==ALLOWED_COMMANDS['validate']: lb.validator.validate()
elif args.command==ALLOWED_COMMANDS['send']: lb.sender.send()
Expand Down
24 changes: 24 additions & 0 deletions lightbeam/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ def get_params_for_endpoint(self, endpoint, type='required'):
definition = util.get_swagger_ref_for_endpoint(self.lightbeam.config["namespace"], swagger, endpoint)
if type=='required':
return self.get_required_params_from_swagger(swagger, definition)
elif type=='all':
return self.get_all_params_from_swagger(swagger, definition)
else:
# descriptor endpoints all have the same structure and identity fields:
if "Descriptor" in endpoint:
Expand All @@ -360,6 +362,28 @@ def get_required_params_from_swagger(self, swagger, definition, prefix=""):
params[prop] = prefix + prop
return params

def get_all_params_from_swagger(self, swagger, definition, prefix=""):
params = {}
schema = util.resolve_swagger_ref(swagger, definition)
if not schema:
self.logger.critical(f"Swagger contains neither `definitions` nor `components.schemas` - check that the Swagger is valid.")

for prop in schema["properties"].keys():
if prop in ["_etag", "id", "link"]: continue
if "required" in schema.keys() and prop in schema["required"]: prop_name = "[required]"+prop
else: prop_name = "[optional]"+prop
if "$ref" in schema["properties"][prop].keys():
params[prop_name] = {}
sub_definition = schema["properties"][prop]["$ref"]
sub_params = self.get_all_params_from_swagger(swagger, sub_definition, prefix=prefix+prop+"_")
for k,v in sub_params.items():
params[prop_name][k] = v
elif schema["properties"][prop]["type"]!="array":
params[prop_name] = f"[{schema['properties'][prop]['type']}]" + prefix + prop
else:
params[prop_name] = [self.get_all_params_from_swagger(swagger, schema["properties"][prop]["items"]["$ref"], prefix=prefix+prop+"-")]
return params

def get_identity_params_from_swagger(self, swagger, definition, prefix=""):
params = {}
schema = util.resolve_swagger_ref(swagger, definition)
Expand Down
105 changes: 105 additions & 0 deletions lightbeam/create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import os
import re
import json
import yaml
from lightbeam import util

class Creator:

def __init__(self, lightbeam=None):
self.lightbeam = lightbeam
self.logger = self.lightbeam.logger
self.template_folder = "templates/"
self.earthmover_file = "earthmover.yml"

def create(self):
self.lightbeam.api.load_swagger_docs()
os.makedirs(self.template_folder, exist_ok=True)
earthmover_yaml = {}
# check if file exists!
if os.path.isfile(self.earthmover_file):
with open(self.earthmover_file) as file:
earthmover_yaml = yaml.safe_load(file)
for endpoint in self.lightbeam.endpoints:
if endpoint in (earthmover_yaml.get("destinations", {}) or {}).keys():
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`).")
self.create_jsont(endpoint)
# write out earthmover_yaml
if not os.path.isfile(self.earthmover_file):
self.logger.info(f"creating file `{self.earthmover_file}`...")
with open(self.earthmover_file, 'w+') as file:
file.write("""version: 2.0

# This is an earthmover.yml file, generated with `lightbeam create`, for creating Ed-Fi JSON payloads
# using earthmover. See https://github.com/edanalytics/earthmover for documentation.

config:
macros: >
{% macro descriptor_namespace() -%}
uri://ed-fi.org
{%- endmacro %}

# Define your source data here:
sources:
# Example:
# mysource:
# file: path/to/myfile.csv
# ...

# (If needed, define your data transformations here:)
# transformations:

destinations:""")
for endpoint in self.lightbeam.endpoints:
file.write(self.create_em_destination_node(endpoint))
else:
self.logger.info(f"appending to file `{self.earthmover_file}`...")
with open(self.earthmover_file, 'a+') as file:
for endpoint in self.lightbeam.endpoints:
file.write(self.create_em_destination_node(endpoint))

def create_em_destination_node(self, endpoint):
return f"""
{endpoint}:
source: $transformations.{endpoint}
template: {self.template_folder}{endpoint}.jsont
extension: jsonl
linearize: True"""

def upper_repl(self, match):
value = match.group(1)
return "/" + value[0].upper() + value[1:] + "Descriptor#"


def create_jsont(self, endpoint):
template_file = f"{self.template_folder}{endpoint}.jsont"
# check if file exists!
if os.path.isfile(template_file):
self.logger.critical(f"The file `{template_file}` already exists in the current directory; to re-create it, please first manually delete it.")
# generate base JSON structure:
content = self.lightbeam.api.get_params_for_endpoint(endpoint, type='all')
# pretty-print it:
content = json.dumps(content, indent=2)
# annotate required/optional properties:
content = content.replace('"[required]', '{# (required) #} "')
content = content.replace('"[optional]', '{# (optional) #} "')
# appropriate quoting based on property data type:
content = re.sub('"\[string\](.*)Descriptor"', r'"{{descriptor_namespace()}}/\1Descriptor#{{\1Descriptor}}"', content)
content = re.sub('/(.*)_(.*)Descriptor#', r'/\2Descriptor#', content)
content = re.sub(r'/(.*)Descriptor#', self.upper_repl, content)
content = re.sub('"\[string\](.*)"', r'"{{\1}}"', content)
content = re.sub('"\[(integer|boolean)\](.*)"', r'{{\2}}', content)
# for loops over arrays:
content = re.sub('"(.*)": \[', r'"\1": [ {% for item in \1 %}', content)
content = re.sub('\]', r'{% endfor %} ]', content)
content = re.sub('{{(.*)-(.*)}}', r'{{item.\2}}', content)
# add info header message:
content = """{#
This is an earthmover JSON template file, generated with `lightbeam create`, for creating Ed-Fi JSON `"""+endpoint+"""`
payloads using earthmover. See https://github.com/edanalytics/earthmover for documentation.
#}
""" + content
# write out json template
self.logger.info(f"creating file `{template_file}`...")
with open(template_file, 'w+') as file:
file.write(content)
2 changes: 2 additions & 0 deletions lightbeam/lightbeam.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from lightbeam import util
from lightbeam.api import EdFiAPI
from lightbeam.count import Counter
from lightbeam.create import Creator
from lightbeam.fetch import Fetcher
from lightbeam.validate import Validator
from lightbeam.send import Sender
Expand Down Expand Up @@ -73,6 +74,7 @@ def __init__(self, config_file, logger=None, selector="*", exclude="", keep_keys
self.endpoints = []
self.results = []
self.counter = Counter(self)
self.creator = Creator(self)
self.fetcher = Fetcher(self)
self.validator = Validator(self)
self.sender = Sender(self)
Expand Down