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""" + +""" + 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 '' + + +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 @@ +NCAR-contemp-logo-blue.a 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + ++++ + +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