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

Add tool to upload to IBM Quantum Learning #358

Merged
merged 20 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from 18 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
66 changes: 66 additions & 0 deletions .github/workflows/learning-uploader-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This code is a Qiskit project.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

name: Test learning platform upload tool
on:
pull_request:
paths:
- "scripts/ibm-quantum-learning-uploader/**"
frankharkins marked this conversation as resolved.
Show resolved Hide resolved
workflow_dispatch:
schedule:
# At 08:00 on Monday (https://crontab.guru/#0_8_*_*_1)
- cron: "0 8 * * 1"
jobs:
e2e:
name: End-to-end test
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install Firefox
uses: browser-actions/setup-firefox@v1
- name: Run test
run: |
cd scripts/ibm-quantum-learning-uploader/test
./e2e-test.sh
env:
LEARNING_API_TOKEN: ${{ secrets.LEARNING_API_TOKEN_STAGING }}
LEARNING_API_ENVIRONMENT: staging

make_issue:
name: Make issue on failure
needs: [e2e]
if: ${{ failure() && github.event_name == 'schedule' }}
runs-on: ubuntu-latest
steps:
- name: Post issue
uses: actions/github-script@v7
with:
script: |
const message = `Today's scheduled e2e test of the upload tool failed.

Please [check the logs](https://github.com/Qiskit/documentation/actions/runs/${context.workflow}).

> [!NOTE]
> This issue was created by a GitHub action.
`

github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: "Learning upload tool test failed",
body: message,
assignees: ["frankharkins"]
})
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

.ipynb_checkpoints/
.DS_Store
__pycache__/

node_modules
tsconfig.tsbuildinfo
Expand Down
57 changes: 57 additions & 0 deletions scripts/ibm-quantum-learning-uploader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Upload to IBM Quantum Learning

This script uploads lessons to [IBM Quantum
Learning](learning.quantum-computing.ibm.com).

Lessons to be uploaded must be a directory containing a single notebook and any
images it uses (the API can't handle image attachments inside the notebooks).

```sh
# Example
lesson-root
├── index.ipynb
└── images
└── image-1.png
```

To sync your lessons automatically with the API, you'll need to add a
`learning-platform.conf.yaml` to the root of your content folder.

```yaml
# learning-platform.conf.yaml
# This goes in the root of your content folder
lessons:
- path: path/to/folder # path to folder containing the lesson
# Lesson IDs in the API:
idStaging: 4e85c04a-c2fb-4bfc-9077-b75bf1b73a25
idProduction: 5026731b-5e7b-4585-8cf2-f24482819e21
```

To upload lessons using our script:

1. Install the package
```bash
pip install git+https://github.com/Qiskit/documentation.git#subdirectory=scripts/ibm-quantum-learning-uploader
```
2. Run this from the root of your content folder:

```bash
sync-lessons
```

You'll be prompted for your username and password.

You can also upload just one lesson at a time using

```bash
sync-lessons path/to/folder
```

To use this script as part of CI (Travis / GitHub actions), set the
`LEARNING_API_TOKEN` environment variable to your access token before running
the script, and `LEARNING_API_ENVIRONMENT` to the name of the API (`staging` or
`production`). You should also pin the package to a specific commit, like so:

```bash
pip install git+https://github.com/Qiskit/documentation.git@<commit-hash>#subdirectory=scripts/ibm-quantum-learning-uploader
```
30 changes: 30 additions & 0 deletions scripts/ibm-quantum-learning-uploader/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "ibm-quantum-learning-uploader"
version = "0.0.1"
authors = [
{ name="Qiskit Development Team", email="hello@qiskit.org" },
]
description = "Tool for IBM Quantum Learning content creators to upload lessons through the API."
readme = "README.md"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache 2.0 License",
"Operating System :: OS Independent",
]
dependencies = [
"pyyaml",
"yaspin",
"requests~=2.28"
]

[project.urls]
"Homepage" = "https://github.com/Qiskit/documentation/tree/main/scripts/ibm-quantum-learning-uploader"
"Bug Tracker" = "https://github.com/Qiskit/documentation/issues"

[project.scripts]
sync-lessons = "learning_uploader:sync_lessons"
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import sys
import os
import yaml
from pathlib import Path
from .upload import Lesson, API

CONF_FILE = "./learning-platform.conf.yaml"
API_URLS = {
"staging": "https://learning-api-dev.quantum-computing.ibm.com",
"production": "https://learning-api.quantum-computing.ibm.com",
}
WEBSITE_URLS = {
"staging": "https://learning.www-dev.quantum-computing.ibm.com",
"production": "https://learning.quantum-computing.ibm.com",
}


def get_api_name():
"""
Either from environment token or user
"""
if os.environ.get("LEARNING_API_ENVIRONMENT", False):
return os.environ.get("LEARNING_API_ENVIRONMENT").lower()

if os.environ.get("LEARNING_API_TOKEN", False):
raise EnvironmentError(
"Set 'LEARNING_API_ENVIRONMENT' variable to 'staging' or "
"'production' when using the 'LEARNING_API_TOKEN' environment "
"variable. You can unset the token using the command:\n\n "
"unset LEARNING_API_TOKEN\n"
)

response = input("Push to staging or production? (s/p): ").lower()
if response in ["s", "staging"]:
return "staging"
if response in ["p", "production"]:
return "production"
print("Not understood; enter either 's' for staging or 'p' for production.")
print("Trying again...")
return get_api_name()


def get_switch(switch, pop=True):
"""
Return True if switch in sys.argv, else False
If `pop`, also remove switch from sys.argv
"""
if switch not in sys.argv:
return False
if pop:
sys.argv.remove(switch)
return True


def check_for_unrecognized_switches():
for arg in sys.argv:
if arg.startswith("-"):
print(f'Unsupported argument: "{arg}"')
sys.exit(1)


def sync_lessons():
if get_switch("--help"):
print(
"Usage: sync-lessons [ path(s)/to/folder(s) ]\n"
"Optional switches:\n"
" --hide-urls: Don't print URLs after uploading a lesson\n"
" --help: Show this message and exit"
)
sys.exit()
hide_urls = get_switch("--hide-urls")
check_for_unrecognized_switches()

api_name = get_api_name()
api = API(
name=api_name,
api_url=API_URLS[api_name],
website_url=WEBSITE_URLS[api_name],
hide_urls=hide_urls,
)

lesson_ids = parse_yaml(api_name)
if len(sys.argv) > 1:
paths = sys.argv[1:]
else:
paths = lesson_ids.keys()

for lesson_path in paths:
lesson = Lesson(lesson_path, lesson_ids[lesson_path])
api.push(lesson)

print("✨ Sync complete! ✨\n")


def parse_yaml(api_name):
"""
Get dict of lesson paths and lesson IDs
Args:
api_name (str): "staging" or "production"
Returns:
dict: { lesson_path: lesson_id }
"""
with open(CONF_FILE) as f:
api_info = yaml.safe_load(f.read())

output = {}
for lesson in api_info["lessons"]:
path = lesson["path"]
lesson_id = lesson[f"id{api_name.lower().capitalize()}"]
output[path] = lesson_id

return output
Loading