diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..437ba14
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,2 @@
+# GLOBAL OWNER
+* @ProjectPythiaTutorials/infrastructure
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b4b3fa4
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+version: 2
+updates:
+ # - package-ecosystem: pip
+ # directory: "/"
+ # schedule:
+ # interval: daily
+ - package-ecosystem: 'github-actions'
+ directory: '/'
+ schedule:
+ # Check for updates once a week
+ interval: 'weekly'
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..47147bb
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,55 @@
+name: deploy-site
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash -l {0}
+ if: github.repository == 'ProjectPythiaTutorials/projectpythiatutorials.github.io'
+ steps:
+ - name: Cancel previous runs
+ uses: styfle/cancel-workflow-action@0.9.1
+ with:
+ access_token: ${{ github.token }}
+ - uses: actions/checkout@v2
+
+ - uses: conda-incubator/setup-miniconda@master
+ with:
+ channels: conda-forge,nodefaults
+ channel-priority: strict
+ activate-environment: pythia
+ auto-update-conda: false
+ python-version: 3.9
+ environment-file: ci/environment.yml
+ mamba-version: '*'
+ use-mamba: true
+
+ - name: Build
+ run: |
+ conda env list
+ cd site
+ make -j4 html
+ - name: Zip the site
+ run: |
+ set -x
+ set -e
+ if [ -f site.zip ]; then
+ rm -rf site.zip
+ fi
+ zip -r site.zip ./site/_build/html
+ - uses: actions/upload-artifact@v2
+ with:
+ name: site-zip
+ path: ./site.zip
+
+ - name: Deploy
+ if: github.ref == 'refs/heads/main'
+ uses: peaceiris/actions-gh-pages@v3.8.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./site/_build/html
+ cname: tutorials.projectpythia.org
diff --git a/.github/workflows/collect-user-submission.py b/.github/workflows/collect-user-submission.py
new file mode 100644
index 0000000..f7c287b
--- /dev/null
+++ b/.github/workflows/collect-user-submission.py
@@ -0,0 +1,84 @@
+import json
+import os
+import typing
+
+import frontmatter
+import pydantic
+from markdown_it import MarkdownIt
+
+
+class Author(pydantic.BaseModel):
+ name: str = 'anonymous'
+ affiliation: str = None
+ affiliation_url: typing.Union[str, pydantic.HttpUrl] = None
+ email: typing.Union[str, pydantic.EmailStr] = None
+
+
+class Submission(pydantic.BaseModel):
+ title: str
+ description: str
+ url: pydantic.HttpUrl
+ thumbnail: typing.Union[str, pydantic.HttpUrl] = None
+ authors: typing.List[Author] = None
+ tags: typing.Dict[str, typing.List[str]] = None
+
+
+@pydantic.dataclasses.dataclass
+class IssueInfo:
+ gh_event_path: pydantic.FilePath
+ submission: Submission = pydantic.Field(default=None)
+
+ def __post_init_post_parse__(self):
+ with open(self.gh_event_path) as f:
+ self.data = json.load(f)
+
+ def create_submission(self):
+ self._get_inputs()
+ self._create_submission_input()
+ return self
+
+ def _get_inputs(self):
+ self.author = self.data['issue']['user']['login']
+ self.title = self.data['issue']['title']
+ self.body = self.data['issue']['body']
+
+ def _create_submission_input(self):
+ md = MarkdownIt()
+ inputs = None
+ for token in md.parse(self.body):
+ if token.tag == 'code':
+ inputs = frontmatter.loads(token.content).metadata
+ break
+ name = inputs.get('name')
+ title = inputs.get('title')
+ description = inputs.get('description')
+ url = inputs.get('url')
+ thumbnail = inputs.get('thumbnail')
+ _authors = inputs.get('authors')
+ authors = []
+ if _authors:
+ for item in _authors:
+ authors.append(
+ Author(
+ name=item.get('name', 'anyonymous'),
+ affiliation=item.get('affiliation'),
+ affiliation_url=item.get('affiliation_url'),
+ email=item.get('email', ''),
+ )
+ )
+ else:
+ authors = [Author(name='anyonymous')]
+ _tags = inputs.get(
+ 'tags', {'packages': ['unspecified'], 'formats': ['unspecified'], 'domains': ['unspecified']}
+ )
+ self.submission = Submission(
+ name=name, title=title, description=description, url=url, thumbnail=thumbnail, authors=authors, tags=_tags
+ )
+
+
+if __name__ == '__main__':
+
+ issue = IssueInfo(gh_event_path=os.environ['GITHUB_EVENT_PATH']).create_submission()
+ inputs = issue.submission.dict()
+ with open('gallery-submission-input.json', 'w') as f:
+ json.dump(inputs, f)
diff --git a/.github/workflows/preview.yaml b/.github/workflows/preview.yaml
new file mode 100644
index 0000000..beb9442
--- /dev/null
+++ b/.github/workflows/preview.yaml
@@ -0,0 +1,130 @@
+name: preview-site
+on:
+ workflow_run:
+ workflows:
+ - deploy-site
+ types:
+ - requested
+ - completed
+jobs:
+ deploy:
+ if: github.repository == 'ProjectPythiaTutorials/projectpythiatutorials.github.io'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set message value
+ run: |
+ echo "comment_message=This pull request is being automatically built with [GitHub Actions](https://github.com/features/actions) and [Netlify](https://www.netlify.com/). To see the status of your deployment, click below." >> $GITHUB_ENV
+ - name: Find Pull Request
+ uses: actions/github-script@v4
+ id: find-pull-request
+ with:
+ script: |
+ let pullRequestNumber = ''
+ let pullRequestHeadSHA = ''
+ core.info('Finding pull request...')
+ const pullRequests = await github.pulls.list({owner: context.repo.owner, repo: context.repo.repo})
+ for (let pullRequest of pullRequests.data) {
+ if(pullRequest.head.sha === context.payload.workflow_run.head_commit.id) {
+ pullRequestHeadSHA = pullRequest.head.sha
+ pullRequestNumber = pullRequest.number
+ break
+ }
+ }
+ core.setOutput('number', pullRequestNumber)
+ core.setOutput('sha', pullRequestHeadSHA)
+ if(pullRequestNumber === '') {
+ core.info(
+ `No pull request associated with git commit SHA: ${context.payload.workflow_run.head_commit.id}`
+ )
+ }
+ else{
+ core.info(`Found pull request ${pullRequestNumber}, with head sha: ${pullRequestHeadSHA}`)
+ }
+ - name: Find Comment
+ uses: peter-evans/find-comment@v1
+ if: steps.find-pull-request.outputs.number != ''
+ id: fc
+ with:
+ issue-number: '${{ steps.find-pull-request.outputs.number }}'
+ comment-author: 'github-actions[bot]'
+ body-includes: '${{ env.comment_message }}'
+ - name: Create comment
+ if: |
+ github.event.workflow_run.conclusion != 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id == ''
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ issue-number: ${{ steps.find-pull-request.outputs.number }}
+ body: |
+ ${{ env.comment_message }}
+
+ 🚧 Deployment in progress for git commit SHA: ${{ steps.find-pull-request.outputs.sha }}
+ - name: Update comment
+ if: |
+ github.event.workflow_run.conclusion != 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id != ''
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ comment-id: ${{ steps.fc.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+ ${{ env.comment_message }}
+
+ 🚧 Deployment in progress for git commit SHA: ${{ steps.find-pull-request.outputs.sha }}
+ - name: Download Artifact site
+ if: |
+ github.event.workflow_run.conclusion == 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id != ''
+ uses: dawidd6/action-download-artifact@v2.14.1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ workflow: ci.yaml
+ run_id: ${{ github.event.workflow_run.id }}
+ name: site-zip
+ - name: Unzip site
+ if: |
+ github.event.workflow_run.conclusion == 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id != ''
+ run: |
+ rm -rf ./site/_build/html
+ unzip site.zip
+ rm -f site.zip
+ # Push the site's HTML to Netlify and get the preview URL
+ - name: Deploy to Netlify
+ if: |
+ github.event.workflow_run.conclusion == 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id != ''
+ id: netlify
+ uses: nwtgck/actions-netlify@v1.2
+ with:
+ publish-dir: ./site/_build/html
+ production-deploy: false
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ enable-commit-comment: false
+ env:
+ NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
+ NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
+ timeout-minutes: 5
+ - name: Update site Preview comment
+ if: |
+ github.event.workflow_run.conclusion == 'success'
+ && steps.find-pull-request.outputs.number != ''
+ && steps.fc.outputs.comment-id != ''
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ comment-id: ${{ steps.fc.outputs.comment-id }}
+ edit-mode: replace
+ body: |
+ ${{ env.comment_message }}
+
+ 🔍 Git commit SHA: ${{ steps.find-pull-request.outputs.sha }}
+ ✅ Deployment Preview URL: ${{ steps.netlify.outputs.deploy-url }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6bf6987
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+site/_build/
+dist/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..2a4f18c
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,49 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.3.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-docstring-first
+ - id: check-json
+ - id: check-yaml
+ - id: double-quote-string-fixer
+
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/keewis/blackdoc
+ rev: v0.3.4
+ hooks:
+ - id: blackdoc
+
+ - repo: https://github.com/PyCQA/flake8
+ rev: 4.0.1
+ hooks:
+ - id: flake8
+
+ - repo: https://github.com/asottile/seed-isort-config
+ rev: v2.2.0
+ hooks:
+ - id: seed-isort-config
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.10.1
+ hooks:
+ - id: isort
+
+ # - repo: https://github.com/prettier/pre-commit
+ # rev: 57f39166b5a5a504d6808b87ab98d41ebf095b46
+ # hooks:
+ # - id: prettier
+
+ - repo: https://github.com/nbQA-dev/nbQA
+ rev: 1.3.1
+ hooks:
+ - id: nbqa-black
+ additional_dependencies: [black==20.8b1]
+ - id: nbqa-pyupgrade
+ additional_dependencies: [pyupgrade==2.7.3]
+ # - id: nbqa-isort
+ # additional_dependencies: [isort==5.6.4]
diff --git a/ci/environment.yml b/ci/environment.yml
new file mode 100644
index 0000000..bf54f33
--- /dev/null
+++ b/ci/environment.yml
@@ -0,0 +1,14 @@
+name: pythia-tutorial-dev
+channels:
+- conda-forge
+- nodefaults
+dependencies:
+- matplotlib
+- myst-nb
+- pandas
+- pip
+- pyyaml
+- pre-commit
+- sphinx-panels
+- pip:
+ - sphinx-pythia-theme
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..870b89f
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,16 @@
+[flake8]
+exclude =
+ignore =E501
+max-line-length = 120
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
+
+[isort]
+known_first_party=
+known_third_party=frontmatter,gallery_generator,markdown_it,pydantic,truncatehtml,yaml
+multi_line_output=3
+include_trailing_comma=True
+force_grid_wrap=0
+combine_as_imports=True
+line_length=120
+skip=
diff --git a/site/Makefile b/site/Makefile
new file mode 100644
index 0000000..d4bb2cb
--- /dev/null
+++ b/site/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/site/_extensions/cookbook_gallery_generator.py b/site/_extensions/cookbook_gallery_generator.py
new file mode 100644
index 0000000..d55b941
--- /dev/null
+++ b/site/_extensions/cookbook_gallery_generator.py
@@ -0,0 +1,17 @@
+import yaml
+from gallery_generator import build_from_items, generate_menu
+
+
+def main(app):
+
+ with open('cookbook_gallery.yaml') as fid:
+ all_items = yaml.safe_load(fid)
+
+ title = 'Cookbooks Gallery'
+ subtext = 'Pythia Cookbooks provide example workflows on more advanced and domain-specific problems developed by the Pythia community. Cookbooks build on top of skills you learn in Pythia Foundations.'
+ menu_html = generate_menu(all_items)
+ build_from_items(all_items, 'index', title=title, subtext=subtext, menu_html=menu_html)
+
+
+def setup(app):
+ app.connect('builder-inited', main)
\ No newline at end of file
diff --git a/site/_extensions/gallery_generator.py b/site/_extensions/gallery_generator.py
new file mode 100644
index 0000000..34affc4
--- /dev/null
+++ b/site/_extensions/gallery_generator.py
@@ -0,0 +1,181 @@
+import itertools
+import pathlib
+from textwrap import dedent
+
+from truncatehtml import truncate
+
+
+def _generate_sorted_tag_keys(all_items):
+
+ key_set = set(itertools.chain(*[item['tags'].keys() for item in all_items]))
+ return sorted(key_set)
+
+
+def _generate_tag_set(all_items, tag_key=None):
+
+ tag_set = set()
+ for item in all_items:
+ for k, e in item['tags'].items():
+ if tag_key and k != tag_key:
+ continue
+ for t in e:
+ tag_set.add(t)
+
+ return tag_set
+
+
+def _generate_tag_menu(all_items, tag_key):
+
+ tag_set = _generate_tag_set(all_items, tag_key)
+ tag_list = sorted(tag_set)
+
+ options = ''.join(
+ f'
'
+ for tag in tag_list
+ )
+
+ return f"""
+
+
+
+
+
+"""
+
+
+def generate_menu(all_items, submit_btn_txt=None, submit_btn_link=None):
+
+ key_list = _generate_sorted_tag_keys(all_items)
+
+ menu_html = '\n'
+ menu_html += '\n'
+ menu_html += '
\n'
+ menu_html += '
\n'
+ for tag_key in key_list:
+ menu_html += _generate_tag_menu(all_items, tag_key) + '\n'
+ menu_html += '
\n'
+ menu_html += '
\n'
+ menu_html += '\n'
+ return menu_html
+
+
+def build_from_items(items, filename, title='Gallery', subtitle=None, subtext=None, menu_html='', max_descr_len=300):
+
+ # Build the gallery file
+ panels_body = []
+ for item in items:
+ if not item.get('thumbnail'):
+ item['thumbnail'] = '/_static/images/ebp-logo.png'
+ thumbnail = item['thumbnail']
+ tag_list = sorted((itertools.chain(*item['tags'].values())))
+ tag_list_f = [tag.replace(' ', '-') for tag in tag_list]
+
+ tags = [f'{tag}' for tag in tag_list_f]
+ tags = '\n'.join(tags)
+
+ tag_class_str = ' '.join(tag_list_f)
+
+ author_strs = set()
+ institution_strs = set()
+ for a in item['authors']:
+ author_name = a.get('name', 'Anonymous')
+ author_email = a.get('email', None)
+ if author_email:
+ _str = f'{author_name}'
+ else:
+ _str = author_name
+ author_strs.add(_str)
+
+ institution_name = a.get('institution', None)
+ if institution_name:
+ institution_url = a.get('institution_url', None)
+ if institution_url:
+ _str = f'{institution_name}'
+ else:
+ _str = institution_name
+ institution_strs.add(_str)
+
+ authors_str = f"Author: {', '.join(author_strs)}"
+ if institution_strs:
+ institutions_str = f"Institution: {' '.join(institution_strs)}"
+ else:
+ institutions_str = ''
+
+ ellipsis_str = ' ... more'
+ short_description = truncate(item['description'], max_descr_len, ellipsis=ellipsis_str)
+
+ if ellipsis_str in short_description:
+ modal_str = f"""
+
+
+

+
{item["title"]}
+{authors_str}
+
+{institutions_str}
+
{item['description']}
+
{tags}
+
Visit Website
+
+
+"""
+ else:
+ modal_str = ''
+
+ panels_body.append(
+ f"""\
+---
+:column: + tagged-card {tag_class_str}
+
+
+

+
+
+{modal_str}
+
++++
+
+{tags}
+
+"""
+ )
+
+ panels_body = '\n'.join(panels_body)
+
+ stitle = f'#### {subtitle}' if subtitle else ''
+ stext = subtext if subtext else ''
+
+ panels = f"""
+# {title}
+
+{stitle}
+{stext}
+
+{menu_html}
+
+````{{panels}}
+:column: col-12
+:card: +mb-4 w-100
+:header: d-none
+:body: p-3 m-0
+:footer: p-1
+
+{dedent(panels_body)}
+````
+
+
+
+"""
+
+ pathlib.Path(f'{filename}.md').write_text(panels)
\ No newline at end of file
diff --git a/site/_extensions/truncatehtml.py b/site/_extensions/truncatehtml.py
new file mode 100644
index 0000000..8d754af
--- /dev/null
+++ b/site/_extensions/truncatehtml.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2015 Eric Entzel
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import print_function
+
+END = -1
+
+# HTML5 void-elements that do not require a closing tag
+# https://html.spec.whatwg.org/multipage/syntax.html#void-elements
+VOID_ELEMENTS = (
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'link',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr',
+)
+
+
+class UnbalancedError(Exception):
+ pass
+
+
+class OpenTag:
+ def __init__(self, tag, rest=''):
+ self.tag = tag
+ self.rest = rest
+
+ def as_string(self):
+ return '<' + self.tag + self.rest + '>'
+
+
+class CloseTag(OpenTag):
+ def as_string(self):
+ return '' + self.tag + '>'
+
+
+class SelfClosingTag(OpenTag):
+ pass
+
+
+class Tokenizer:
+ def __init__(self, input):
+ self.input = input
+ self.counter = 0 # points at the next unconsumed character of the input
+
+ def __next_char(self):
+ self.counter += 1
+ return self.input[self.counter]
+
+ def next_token(self):
+ try:
+ char = self.input[self.counter]
+ self.counter += 1
+ if char == '&':
+ return self.__entity()
+ elif char != '<':
+ return char
+ elif self.input[self.counter] == '/':
+ self.counter += 1
+ return self.__close_tag()
+ else:
+ return self.__open_tag()
+ except IndexError:
+ return END
+
+ def __entity(self):
+ """Return a token representing an HTML character entity.
+ Precondition: self.counter points at the charcter after the &
+ Postcondition: self.counter points at the character after the ;
+ """
+ next_char = self.input[self.counter + 1]
+ if next_char == ' ':
+ return '&'
+
+ char = self.input[self.counter]
+ entity = ['&']
+ while char != ';':
+ entity.append(char)
+ char = self.__next_char()
+ entity.append(';')
+ self.counter += 1
+ return ''.join(entity)
+
+ def __open_tag(self):
+ """Return an open/close tag token.
+ Precondition: self.counter points at the first character of the tag name
+ Postcondition: self.counter points at the character after the
+ """
+ char = self.input[self.counter]
+ tag = []
+ rest = []
+ while char != '>' and char != ' ':
+ tag.append(char)
+ char = self.__next_char()
+ while char != '>':
+ rest.append(char)
+ char = self.__next_char()
+ if self.input[self.counter - 1] == '/':
+ self.counter += 1
+ return SelfClosingTag(''.join(tag), ''.join(rest))
+ elif ''.join(tag) in VOID_ELEMENTS:
+ self.counter += 1
+ return SelfClosingTag(''.join(tag), ''.join(rest))
+ else:
+ self.counter += 1
+ return OpenTag(''.join(tag), ''.join(rest))
+
+ def __close_tag(self):
+ """Return an open/close tag token.
+ Precondition: self.counter points at the first character of the tag name
+ Postcondition: self.counter points at the character after the
+ """
+ char = self.input[self.counter]
+ tag = []
+ while char != '>':
+ tag.append(char)
+ char = self.__next_char()
+ self.counter += 1
+ return CloseTag(''.join(tag))
+
+
+def truncate(str, target_len, ellipsis=''):
+ """Returns a copy of str truncated to target_len characters,
+ preserving HTML markup (which does not count towards the length).
+ Any tags that would be left open by truncation will be closed at
+ the end of the returned string. Optionally append ellipsis if
+ the string was truncated."""
+ stack = [] # open tags are pushed on here, then popped when the matching close tag is found
+ retval = [] # string to be returned
+ length = 0 # number of characters (not counting markup) placed in retval so far
+ tokens = Tokenizer(str)
+ tok = tokens.next_token()
+ while tok != END:
+ if length >= target_len and tok == ' ':
+ retval.append(ellipsis)
+ break
+ if tok.__class__.__name__ == 'OpenTag':
+ stack.append(tok)
+ retval.append(tok.as_string())
+ elif tok.__class__.__name__ == 'CloseTag':
+ if stack[-1].tag == tok.tag:
+ stack.pop()
+ retval.append(tok.as_string())
+ else:
+ raise UnbalancedError(tok.as_string())
+ elif tok.__class__.__name__ == 'SelfClosingTag':
+ retval.append(tok.as_string())
+ else:
+ retval.append(tok)
+ length += 1
+ tok = tokens.next_token()
+ while len(stack) > 0:
+ tok = CloseTag(stack.pop().tag)
+ retval.append(tok.as_string())
+ return ''.join(retval)
\ No newline at end of file
diff --git a/site/_static/custom.css b/site/_static/custom.css
new file mode 100644
index 0000000..5f30d91
--- /dev/null
+++ b/site/_static/custom.css
@@ -0,0 +1,120 @@
+main.banner-main #project-pythia {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+ }
+
+ main.banner-main #project-pythia p {
+ font-size: 1.4rem; /* default: 1.25rem */
+ /* font-weight: 700; default: 300 */
+ }
+
+ main.banner-main #project-pythia a,
+ main.banner-main #project-pythia a:visited {
+ color: rgba(var(--spt-color-light), 1);
+ text-decoration: underline dotted rgba(var(--spt-color-gray-400), 1);
+ }
+
+ main.banner-main #project-pythia a.headerlink:hover {
+ color: #DDD;
+ }
+
+ main.banner-main #project-pythia a.btn-light {
+ color: rgba(var(--pst-color-primary), 1)
+ }
+
+ .modal {
+ display: none;
+ position: fixed;
+ background: #f8f9fa;
+ border-radius: 5px;
+ padding: 3rem;
+ width: calc(100% - 8rem);
+ height: auto !important;
+ max-height: calc(100% - 8rem);
+ overflow: scroll;
+ top: 4rem;
+ left: 4rem;
+ z-index: 20001;
+ }
+
+ .modal-backdrop {
+ display: none;
+ position: fixed;
+ background: rgba(0, 0, 0, 0.5);
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 100vw;
+ z-index: 20000;
+ }
+
+ .modal-btn {
+ color: #1a658f;
+ text-decoration: none;
+ }
+
+ .modal-img {
+ float: right;
+ margin: 0 0 2rem 2rem;
+ max-width: 260px;
+ max-height: 260px;
+ }
+
+ .gallery-menu {
+ margin-bottom: 1rem;
+ }
+
+ .gallery-card div.container {
+ padding: 0 0 0 1rem;
+ }
+
+ .gallery-thumbnail {
+ display: block;
+ float: left;
+ margin: auto 0;
+ padding: 0;
+ max-width: 160px;
+ }
+
+ .card-subtitle {
+ font-size: 0.8rem;
+ }
+
+ @media (max-width: 576px) {
+ .modal {
+ padding: 2rem;
+ width: calc(100% - 4rem);
+ max-height: calc(100% - 4rem);
+ top: 2rem;
+ left: 2rem;
+ }
+
+ .modal-img {
+ display: none;
+ }
+
+ .gallery-card {
+ flex-direction: column;
+ }
+
+ .gallery-thumbnail {
+ float: none;
+ margin: 0 0 1rem 0;
+ max-width: 100%;
+ }
+
+ .gallery-card div.container {
+ padding: 0;
+ }
+
+ .gallery-return-btn {
+ padding-bottom: 1rem;
+ }
+ }
+
+ div.horizontalgap {
+ float: left;
+ overflow: hidden;
+ height: 1px;
+ width: 0px;
+ }
\ No newline at end of file
diff --git a/site/_static/custom.js b/site/_static/custom.js
new file mode 100644
index 0000000..935a4e6
--- /dev/null
+++ b/site/_static/custom.js
@@ -0,0 +1,184 @@
+var buttons = document.querySelectorAll('.modal-btn')
+var backdrop = document.querySelector('.modal-backdrop')
+var modals = document.querySelectorAll('.modal')
+
+function openModal(i) {
+ backdrop.style.display = 'block'
+ modals[i].style.display = 'block'
+}
+
+function closeModal(i) {
+ backdrop.style.display = 'none'
+ modals[i].style.display = 'none'
+}
+
+for (i = 0; i < buttons.length; i++) {
+ buttons[i].addEventListener(
+ 'click',
+ (function (j) {
+ return function () {
+ openModal(j)
+ }
+ })(i)
+ )
+ backdrop.addEventListener(
+ 'click',
+ (function (j) {
+ return function () {
+ closeModal(j)
+ }
+ })(i)
+ )
+}
+
+
+function change() {
+ var affiliationCbs = document.querySelectorAll(".affiliation input[type='checkbox']");
+ var domainsCbs = document.querySelectorAll(".domains input[type='checkbox']");
+ var formatsCbs = document.querySelectorAll(".formats input[type='checkbox']");
+ var packagesCbs = document.querySelectorAll(".packages input[type='checkbox']");
+
+ var filters = {
+ affiliation: getClassOfCheckedCheckboxes(affiliationCbs),
+ domains: getClassOfCheckedCheckboxes(domainsCbs),
+ formats: getClassOfCheckedCheckboxes(formatsCbs),
+ packages: getClassOfCheckedCheckboxes(packagesCbs)
+ };
+
+ filterResults(filters);
+}
+
+function getClassOfCheckedCheckboxes(checkboxes) {
+ var classes = [];
+
+ if (checkboxes && checkboxes.length > 0) {
+ for (var i = 0; i < checkboxes.length; i++) {
+ var cb = checkboxes[i];
+
+ if (cb.checked) {
+ classes.push(cb.getAttribute("rel"));
+ }
+ }
+ }
+
+ return classes;
+}
+
+function filterResults(filters) {
+ var rElems = document.querySelectorAll(".tagged-card");
+ var hiddenElems = [];
+
+ if (!rElems || rElems.length <= 0) {
+ return;
+ }
+
+ for (var i = 0; i < rElems.length; i++) {
+ var el = rElems[i];
+
+ if (filters.affiliation.length > 0) {
+ var isHidden = true;
+
+ for (var j = 0; j < filters.affiliation.length; j++) {
+ var filter = filters.affiliation[j];
+
+ if (el.classList.contains(filter)) {
+ isHidden = false;
+ break;
+ }
+ }
+
+ if (isHidden) {
+ hiddenElems.push(el);
+ }
+ }
+
+ if (filters.domains.length > 0) {
+ var isHidden = true;
+
+ for (var j = 0; j < filters.domains.length; j++) {
+ var filter = filters.domains[j];
+
+ if (el.classList.contains(filter)) {
+ isHidden = false;
+ break;
+ }
+ }
+
+ if (isHidden) {
+ hiddenElems.push(el);
+ }
+ }
+
+ if (filters.formats.length > 0) {
+ var isHidden = true;
+
+ for (var j = 0; j < filters.formats.length; j++) {
+ var filter = filters.formats[j];
+
+ if (el.classList.contains(filter)) {
+ isHidden = false;
+ break;
+ }
+ }
+
+ if (isHidden) {
+ hiddenElems.push(el);
+ }
+ }
+
+ if (filters.packages.length > 0) {
+ var isHidden = true;
+
+ for (var j = 0; j < filters.packages.length; j++) {
+ var filter = filters.packages[j];
+
+ if (el.classList.contains(filter)) {
+ isHidden = false;
+ break;
+ }
+ }
+
+ if (isHidden) {
+ hiddenElems.push(el);
+ }
+ }
+ }
+
+ for (var i = 0; i < rElems.length; i++) {
+ rElems[i].classList.replace("d-none", "d-flex");
+ }
+
+ if (hiddenElems.length <= 0) {
+ return;
+ }
+
+ for (var i = 0; i < hiddenElems.length; i++) {
+ hiddenElems[i].classList.replace("d-flex", "d-none");
+ }
+}
+
+
+function clearCbs() {
+ var affiliationCbs = document.querySelectorAll(".affiliation input[type='checkbox']");
+ var domainsCbs = document.querySelectorAll(".domains input[type='checkbox']");
+ var formatsCbs = document.querySelectorAll(".formats input[type='checkbox']");
+ var packagesCbs = document.querySelectorAll(".packages input[type='checkbox']");
+
+ for (var i = 0; i < affiliationCbs.length; i++) {
+ affiliationCbs[i].checked=false;
+ }
+
+ for (var i = 0; i < domainsCbs.length; i++) {
+ domainsCbs[i].checked=false;
+ }
+
+ for (var i = 0; i < formatsCbs.length; i++) {
+ formatsCbs[i].checked=false;
+ }
+
+ for (var i = 0; i < packagesCbs.length; i++) {
+ packagesCbs[i].checked=false;
+ }
+
+ change();
+}
\ No newline at end of file
diff --git a/site/_static/images/icons/favicon.ico b/site/_static/images/icons/favicon.ico
new file mode 100644
index 0000000..da6ac73
Binary files /dev/null and b/site/_static/images/icons/favicon.ico differ
diff --git a/site/_static/images/logos/NCAR-contemp-logo-blue.svg b/site/_static/images/logos/NCAR-contemp-logo-blue.svg
new file mode 100644
index 0000000..3bcda63
--- /dev/null
+++ b/site/_static/images/logos/NCAR-contemp-logo-blue.svg
@@ -0,0 +1 @@
+
diff --git a/site/_static/images/logos/UAlbany-A2-logo-purple-gold.svg b/site/_static/images/logos/UAlbany-A2-logo-purple-gold.svg
new file mode 100644
index 0000000..4fdfe3a
--- /dev/null
+++ b/site/_static/images/logos/UAlbany-A2-logo-purple-gold.svg
@@ -0,0 +1,1125 @@
+
+
diff --git a/site/_static/images/logos/Unidata_logo_horizontal_1200x300.svg b/site/_static/images/logos/Unidata_logo_horizontal_1200x300.svg
new file mode 100644
index 0000000..0d9fd70
--- /dev/null
+++ b/site/_static/images/logos/Unidata_logo_horizontal_1200x300.svg
@@ -0,0 +1,891 @@
+
+
diff --git a/site/_static/images/logos/footer-logo-nsf.png b/site/_static/images/logos/footer-logo-nsf.png
new file mode 100644
index 0000000..11c788f
Binary files /dev/null and b/site/_static/images/logos/footer-logo-nsf.png differ
diff --git a/site/_static/images/logos/pythia_logo-white-rtext.svg b/site/_static/images/logos/pythia_logo-white-rtext.svg
new file mode 100644
index 0000000..fa2a5c6
--- /dev/null
+++ b/site/_static/images/logos/pythia_logo-white-rtext.svg
@@ -0,0 +1,225 @@
+
+
diff --git a/site/_static/images/thumbnails/arm_logo.png b/site/_static/images/thumbnails/arm_logo.png
new file mode 100644
index 0000000..8b95ec1
Binary files /dev/null and b/site/_static/images/thumbnails/arm_logo.png differ
diff --git a/site/_templates/footer-extra.html b/site/_templates/footer-extra.html
new file mode 100644
index 0000000..7a046ac
--- /dev/null
+++ b/site/_templates/footer-extra.html
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/site/_templates/footer-menu.html b/site/_templates/footer-menu.html
new file mode 100644
index 0000000..6706970
--- /dev/null
+++ b/site/_templates/footer-menu.html
@@ -0,0 +1,33 @@
+{% set ext_icon = '' %}
+
\ No newline at end of file
diff --git a/site/conf.py b/site/conf.py
new file mode 100644
index 0000000..49460f9
--- /dev/null
+++ b/site/conf.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import shutil
+import sys
+
+sys.path.insert(0, os.path.abspath('_extensions'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Project Pythia Cookbooks'
+author = 'the Project Pythia Community'
+copyright = '2022'
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'myst_nb',
+ 'sphinx_panels',
+ 'cookbook_gallery_generator',
+]
+
+# Define what extensions will parse which kind of source file
+source_suffix = {
+ '.ipynb': 'myst-nb',
+ '.myst': 'myst-nb',
+}
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_pythia_theme'
+html_last_updated_fmt = '%-d %B %Y'
+
+# Logo & Title
+html_logo = '_static/images/logos/pythia_logo-white-rtext.svg'
+html_title = ''
+
+# Favicon
+html_favicon = '_static/images/icons/favicon.ico'
+
+# Permalinks Icon
+html_permalinks_icon = ''
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+html_css_files = ['custom.css']
+# html_js_files = ['custom.js']
+
+# Disable Sidebars on special pages
+html_sidebars = {
+ 'index': [],
+}
+
+# HTML Theme-specific Options
+html_theme_options = {
+ 'google_analytics_id': 'G-T9KGMX7VHZ',
+ 'github_url': 'https://github.com/ProjectPythia',
+ 'twitter_url': 'https://twitter.com/project_pythia',
+ 'icon_links': [
+ {
+ 'name': 'YouTube',
+ 'url': 'https://www.youtube.com/channel/UCoZPBqJal5uKpO8ZiwzavCw',
+ 'icon': 'fab fa-youtube-square',
+ 'type': 'fontawesome',
+ }
+ ],
+ 'logo_link': 'https://projectpythia.org',
+ 'navbar_links': [
+ {'name': 'Home', 'url': 'https://projectpythia.org'},
+ {'name': 'Foundations', 'url': 'https://foundations.projectpythia.org'},
+ {'name': 'Cookbooks', 'url': 'https://cookbooks.projectpythia.org/'},
+ {'name': 'Resources', 'url': 'https://projectpythia.org/resource-gallery.html'},
+ {'name': 'Community', 'url': 'https://projectpythia.org/#join-us'},
+ ],
+ 'page_layouts': {
+ 'index': 'page-standalone.html',
+ },
+ 'footer_logos': {
+ 'NCAR': '_static/images/logos/NCAR-contemp-logo-blue.svg',
+ 'Unidata': '_static/images/logos/Unidata_logo_horizontal_1200x300.svg',
+ 'UAlbany': '_static/images/logos/UAlbany-A2-logo-purple-gold.svg',
+ },
+ 'extra_navbar': ('Theme by Project Pythia'),
+}
+
+# Panels config
+panels_add_bootstrap_css = False
+
+# MyST config
+myst_enable_extensions = ['amsmath', 'colon_fence', 'deflist', 'html_image']
+myst_url_schemes = ['http', 'https', 'mailto']
+jupyter_execute_notebooks = 'off'
+myst_heading_anchors = 3
+
+# CUSTOM SCRIPTS ==============================================================
\ No newline at end of file
diff --git a/site/cookbook_gallery.yaml b/site/cookbook_gallery.yaml
new file mode 100644
index 0000000..03e5838
--- /dev/null
+++ b/site/cookbook_gallery.yaml
@@ -0,0 +1,12 @@
+- title: Radar Cookbook
+ url: https://projectpythiatutorials.github.io/radar-cookbook/landing-page.html
+ description: |
+ This Project Pythia Cookbook covers the basics of working with weather radar data in Python.
+ authors:
+ - name: Max Grover
+ thumbnail: _static/images/thumbnails/arm_logo.png
+ tags:
+ domains:
+ - radar
+ packages:
+ - py-art
\ No newline at end of file
diff --git a/site/index.md b/site/index.md
new file mode 100644
index 0000000..7e94c4b
--- /dev/null
+++ b/site/index.md
@@ -0,0 +1,69 @@
+
+# Cookbooks Gallery
+
+
+Pythia Cookbooks provide example workflows on more advanced and domain-specific problems developed by the Pythia community. Cookbooks build on top of skills you learn in Pythia Foundations.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+````{panels}
+:column: col-12
+:card: +mb-4 w-100
+:header: d-none
+:body: p-3 m-0
+:footer: p-1
+
+---
+:column: + tagged-card py-art radar
+
+
+

+
+
Radar Cookbook
+
Author: Max Grover
+
This Project Pythia Cookbook covers the basics of working with weather radar data in Python.
+
+
+
+
+
++++
+
+py-art
+radar
+
+
+````
+
+
+
diff --git a/site/make.bat b/site/make.bat
new file mode 100644
index 0000000..2119f51
--- /dev/null
+++ b/site/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd