def validate_appyter(appyter):
    print(f"{appyter}: Checking for existing of files...")
    assert os.path.isfile(
        os.path.join('appyters', appyter,
                     'README.md')), f"Missing appyters/{appyter}/README.md"
    assert os.path.isfile(os.path.join(
        'appyters', appyter,
        'appyter.json')), f"Missing appyters/{appyter}/appyter.json"
    #
    print(f"{appyter}: Validating `{appyter}/appyter.json`...")
    config = json.load(
        open(os.path.join('appyters', appyter, 'appyter.json'), 'r'))
    validator = jsonschema.Draft7Validator({
        '$ref':
        f"file:///{os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'schema', 'appyter-validator.json'))}",
    })
    errors = [error.message for error in validator.iter_errors(config)]
    assert errors == [], '\n'.join(errors)
    #
    name = config['name']
    assert name == appyter, f"The directory should be named like `name`"
    #
    nbfile = config['appyter']['file']
    #
    print(f"{appyter}: Preparing docker to run `{nbfile}`...")
    assert os.path.isfile(os.path.join(
        'appyters', appyter, nbfile)), f"Missing appyters/{appyter}/{nbfile}"
    try:
        json.load(open(os.path.join('appyters', appyter, nbfile), 'r'))
    except Exception as e:
        print(f"{nbfile} is not valid json")
        traceback.print_exc()
    #
    assert not os.path.isfile(os.path.join(
        'appyters', appyter,
        'Dockerfile')), 'Custom Dockerfiles are no longer supported'
    print(f"{appyter}: Creating Dockerfile...")
    import sys
    sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
    from compose.build_dockerfile import prepare_appyter
    with open(os.path.join('appyters', appyter, 'Dockerfile'), 'w') as fw:
        print(prepare_appyter(os.path.join('appyters', appyter), config),
              file=fw)
    #
    print(f"{appyter}: Building Dockerfile...")
    with Popen([
            'docker',
            'build',
            '-t',
            f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
            '.',
    ],
               cwd=os.path.join('appyters', appyter),
               stdout=PIPE) as p:
        for line in filter(None, map(str.strip, map(bytes.decode, p.stdout))):
            print(f"{appyter}: `docker build .`: {line}")
        assert p.wait() == 0, '`docker build .` command failed'
    #
    print(f"{appyter}: Inspecting appyter...")
    with Popen([
            'docker',
            'run',
            f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
            'appyter',
            'nbinspect',
            nbfile,
    ],
               stdout=PIPE) as p:
        nbinspect_output = p.stdout.read().decode().strip()
        print(f"{appyter}: `appyter nbinspect {nbfile}`: {nbinspect_output})")
        assert p.wait() == 0, f"`appyter nbinspect {nbfile}` command failed"
    #
    inspect = json.loads(nbinspect_output)
    field_args = {field['args']['name']: field['args'] for field in inspect}
    assert len(field_args) == len(
        inspect
    ), "Some of your fields weren't captured, there might be duplicate `name`s"
    #
    print(f"{appyter}: Preparing defaults...")
    tmp_directory = os.path.realpath('.tmp')
    os.makedirs(tmp_directory, exist_ok=True)
    default_args = {
        field_name: field.get('default')
        for field_name, field in field_args.items()
    }
    file_fields = {
        field['args']['name']
        for field in inspect if field['field'] == 'FileField'
    }
    for file_field in file_fields:
        field_examples = field_args[file_field].get('examples', {})
        default_file = default_args[file_field]
        if default_file:
            if default_file in field_examples:
                print(
                    f"{appyter}: Downloading example file {default_file} from {field_examples[default_file]}..."
                )
                try:
                    urllib.request.urlretrieve(field_examples[default_file],
                                               filename=os.path.join(
                                                   tmp_directory,
                                                   default_file))
                except urllib.error.HTTPError as e:
                    assert e.getcode(
                    ) != 404, f"File not found on remote, reported 404"
                    print(
                        f"{appyter}: WARNING, example file {default_file} from {field_examples[default_file]} resulted in error code {e.getcode()}."
                    )
                    print(
                        f"{appyter}: WARNING,  Stopping early as download requires manual intervention."
                    )
                    return
            else:
                print(
                    f"{appyter}: WARNING, default file isn't in examples, we won't know how to get it if it isn't available in the image"
                )
        else:
            print(f"{appyter}: WARNING, no default file is provided")
    #
    print(f"{appyter}: Constructing default notebook from appyter...")
    with Popen([
            'docker',
            'run',
            '-v',
            f"{tmp_directory}:/data",
            "-i",
            f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
            'appyter',
            'nbconstruct',
            f"--output=/data/{nbfile}",
            nbfile,
    ],
               stdin=PIPE,
               stdout=PIPE) as p:
        print(f"{appyter}: `appyter nbconstruct {nbfile}` < {default_args}")
        stdout, _ = p.communicate(json.dumps(default_args).encode())
        for line in filter(None, map(str.strip, map(bytes.decode, stdout))):
            print(f"{appyter}: `appyter nbconstruct {nbfile}`: {line}")
        assert p.wait() == 0, f"`appyter nbconstruct {nbfile}` command failed"
        assert os.path.exists(
            os.path.join(tmp_directory, config['appyter']
                         ['file'])), 'nbconstruct output was not created'
    #
    print(f"{appyter}: Executing default notebook with appyter...")
    with Popen([
            'docker',
            'run',
            '-v',
            f"{tmp_directory}:/data",
            '-e',
            'PYTHONPATH=/app',
            f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
            'appyter',
            'nbexecute',
            f"--cwd=/data",
            f"/data/{nbfile}",
    ],
               stdout=PIPE) as p:
        for msg in map(json.loads, p.stdout):
            assert msg['type'] != 'error', f"{appyter}: error {msg.get('data')}"
            print(
                f"{appyter}: `appyter nbexecute {nbfile}`: {json.dumps(msg)}")
        assert p.wait() == 0, f"`appyter nbexecute {nbfile}` command failed"
    #
    print(f"{appyter}: Success!")
예제 #2
0
def validate_appyter(appyter):
  print(f"{appyter}: Preparing temporary directory...")
  tmp_directory = os.path.realpath('.tmp')
  os.makedirs(tmp_directory, exist_ok=True)
  #
  print(f"{appyter}: Checking for existing of files...")
  assert os.path.isfile(os.path.join('appyters', appyter, 'README.md')), f"Missing appyters/{appyter}/README.md"
  assert os.path.isfile(os.path.join('appyters', appyter, 'appyter.json')), f"Missing appyters/{appyter}/appyter.json"
  #
  print(f"{appyter}: Validating `{appyter}/appyter.json`...")
  config = json.load(open(os.path.join('appyters', appyter, 'appyter.json'), 'r'))
  validator = jsonschema.Draft7Validator({
    '$ref': f"file:///{os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'schema', 'appyter-validator.json'))}",
  })
  errors = [error.message for error in validator.iter_errors(config)]
  assert errors == [], '\n'.join(errors)
  #
  name = config['name']
  assert name == appyter, f"The directory should be named like `name`"
  #
  if 'image' in config:
    image = config['image']
    if re.match(r'^https?://', image):
      image_name = os.path.basename(image)
      image_path = os.path.join(tmp_directory, image_name)
      print(f"{appyter}: WARNING it is recommended to use a relative path instead of a url")
      _, response = urllib.request.urlretrieve(config['image'], filename=os.path.join(tmp_directory, image_name))
      assert response.get_content_maintype() == 'image', 'Expected image content'
    else:
      image_path = f"appyters/{appyter}/static/{image}"
    #
    with Image.open(image_path, 'r') as img:
      assert img.size == (1280, 720), "Image should be 1280x720 px"
  else:
    print(f"{appyter}: WARNING `{appyter}/appyter.json` should have an 'image' defined...")
  #
  nbfile = config['appyter']['file']
  nbpath = os.path.join('appyters', appyter, nbfile)
  #
  print(f"{appyter}: Checking notebook for issues..")
  nb = nbf.read(open(nbpath, 'r'), as_version=4)
  for cell in nb.cells:
    if cell['cell_type'] == 'code':
      assert not cell.get('execution_count'), "Please clear all notebook output & metadata"
      assert not cell.get('metadata'), "Please clear all notebook output & metadata"
      assert not cell.get('outputs'), "Please clear all notebook output & metadata"
  assert not nb['metadata'].get('widgets'), "Please clear all notebook output & metadata"
  assert not nb['metadata'].get('execution_info'), "Please clear all notebook output & metadata"
  #
  print(f"{appyter}: Preparing docker to run `{nbfile}`...")
  assert os.path.isfile(nbpath), f"Missing {nbpath}"
  try:
    json.load(open(nbpath, 'r'))
  except Exception as e:
    print(f"{nbfile} is not valid json")
    print(f"{appyter}: {traceback.format_exc()}")
  #
  assert not os.path.isfile(os.path.join('appyters', appyter, 'Dockerfile')), 'Custom Dockerfiles are no longer supported'
  print(f"{appyter}: Creating Dockerfile...")
  import sys; sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
  from compose.build_dockerfile import prepare_appyter
  with open(os.path.join('appyters', appyter, 'Dockerfile'), 'w') as fw:
    print(prepare_appyter(os.path.join('appyters', appyter), config), file=fw)
  #
  print(f"{appyter}: Building Dockerfile...")
  with Popen([
    'docker', 'build',
    '-t', f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
    '.',
  ], cwd=os.path.join('appyters', appyter), stdout=PIPE, stderr=STDOUT) as p:
    for line in filter(None, map(str.strip, map(bytes.decode, p.stdout))):
      print(f"{appyter}: `docker build .`: {line}")
    assert p.wait() == 0, '`docker build .` command failed'
  #
  print(f"{appyter}: Inspecting appyter...")
  with Popen([
    'docker', 'run',
    '-e', 'APPYTER_PREFIX=', # hotfix because prefix is baked-in and necessary at production initialization time, but should be empty here
    f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
    'appyter', 'nbinspect',
    nbfile,
  ], stdout=PIPE, stderr=STDOUT) as p:
    nbinspect_output = p.stdout.read().decode().strip()
    print(f"{appyter}: `appyter nbinspect {nbfile}`: {nbinspect_output})")
    assert p.wait() == 0, f"`appyter nbinspect {nbfile}` command failed"
  #
  inspect = json.loads(nbinspect_output)
  field_args = {
    field['args']['name']: field['args']
    for field in inspect
  }
  assert len(field_args) == len(inspect), "Some of your fields weren't captured, there might be duplicate `name`s"
  #
  print(f"{appyter}: Preparing defaults...")
  default_args = {
    field_name: field.get('default')
    for field_name, field in field_args.items()
  }
  file_fields = {
    field['args']['name']
    for field in inspect
    if field['field'] == 'FileField'
  }
  early_stopping = False
  for file_field in file_fields:
    field_examples = field_args[file_field].get('examples', {})
    default_file = default_args[file_field]
    if default_file:
      if default_file in field_examples:
        if os.path.exists(os.path.join('appyters', appyter, field_examples[default_file])):
          print(f"{appyter}: Copying example file {default_file} from {field_examples[default_file]}...")
          shutil.copyfile(os.path.join('appyters', appyter, field_examples[default_file]), os.path.join(tmp_directory, default_file))
        else:
          print(f"{appyter}: Downloading example file {default_file} from {field_examples[default_file]}...")
          try:
            _, response = urllib.request.urlretrieve(field_examples[default_file], filename=os.path.join(tmp_directory, default_file))
            assert response.get_content_type() != 'text/html', 'Expected data, got html'
          except AssertionError as e:
            print(f"{appyter}: WARNING, example file {default_file} from {field_examples[default_file]} resulted in error {str(e)}.")
            early_stopping = True
          except urllib.error.HTTPError as e:
            assert e.getcode() != 404, f"File not found on remote, reported 404"
            print(f"{appyter}: WARNING, example file {default_file} from {field_examples[default_file]} resulted in error code {e.getcode()}.")
            early_stopping = True
      else:
        print(f"{appyter}: WARNING, default file isn't in examples, we won't know how to get it if it isn't available in the image")
    else:
      print(f"{appyter}: WARNING, no default file is provided")
  #
  if early_stopping:
    print(f"{appyter}: WARNING, Stopping early as a download requires manual intervention.")
    return
  print(f"{appyter}: Fixing permissions...")
  assert Popen(['chmod', '-R', '777', tmp_directory]).wait() == 0, f"{appyter}: ERROR: Changing permissions failed"
  print(f"{appyter}: Constructing default notebook from appyter...")
  with Popen([
    'docker', 'run',
    '-v', f"{tmp_directory}:/data",
    "-i", f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
    'appyter', 'nbconstruct',
    f"--output=/data/{nbfile}",
    nbfile,
  ], stdin=PIPE, stdout=PIPE, stderr=STDOUT) as p:
    print(f"{appyter}: `appyter nbconstruct {nbfile}` < {default_args}")
    stdout, _ = p.communicate(json.dumps(default_args).encode())
    for line in filter(None, map(str.strip, stdout.decode().splitlines())):
      print(f"{appyter}: `appyter nbconstruct {nbfile}`: {line}")
    assert p.wait() == 0, f"`appyter nbconstruct {nbfile}` command failed"
    assert os.path.exists(os.path.join(tmp_directory, config['appyter']['file'])), 'nbconstruct output was not created'
  #
  print(f"{appyter}: Executing default notebook with appyter...")
  with Popen([
    'docker', 'run',
    '-v', f"{tmp_directory}:/data",
    '-e', 'PYTHONPATH=/app',
    f"maayanlab/appyters-{config['name'].lower()}:{config['version']}",
    'appyter', 'nbexecute',
    f"--cwd=/data",
    f"{nbfile}",
  ], stdout=PIPE, stderr=STDOUT) as p:
    for msg in map(try_json_loads, p.stdout):
      assert not (type(msg) == dict and msg['type'] == 'error'), f"{appyter}: error {msg.get('data')}"
      print(f"{appyter}: `appyter nbexecute {nbfile}`: {json.dumps(msg)}")
    assert p.wait() == 0, f"`appyter nbexecute {nbfile}` command failed"
  #
  print(f"{appyter}: Success!")