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

Generate api data on each push #10609

Merged
merged 12 commits into from
Oct 25, 2020
35 changes: 35 additions & 0 deletions .github/workflows/api.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Update API Data

on:
push:
branches:
- master
paths:
skullydazed marked this conversation as resolved.
Show resolved Hide resolved
- 'keyboards/**'
- 'layouts/community/**'

jobs:
api_data:
runs-on: ubuntu-latest
container: qmkfm/base_container

steps:
- uses: actions/checkout@v2
with:
fetch-depth: 1
persist-credentials: false

- name: Generate API Data
run: qmk generate-api

- name: Upload API Data
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
ACCESS_TOKEN: ${{ secrets.API_TOKEN_GITHUB }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: main
FOLDER: api_data/v1
CLEAN: true
GIT_CONFIG_EMAIL: hello@qmk.fm
REPOSITORY_NAME: qmk/qmk_keyboards
TARGET_FOLDER: v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*.swp
tags
*~
api_data/v1
build/
.build/
*.bak
Expand Down
1 change: 1 addition & 0 deletions api_data/_config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
theme: jekyll-theme-cayman
5 changes: 5 additions & 0 deletions api_data/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# QMK Keyboard Metadata

This directory contains machine parsable data about keyboards supported by QMK. The latest version is always available online at <https://keyboards.qmk.fm>.

Do not edit anything here by hand. It is generated with the `qmk generate-api` command.
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import docs
from . import doctor
from . import flash
from . import generate
from . import hello
from . import info
from . import json
Expand Down
2 changes: 1 addition & 1 deletion lib/python/qmk/cli/c2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def c2json(cli):

# Generate the keymap.json
try:
keymap_json = qmk.keymap.generate(keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'], type='json', keymap=keymap_json['keymap'])
keymap_json = qmk.keymap.generate_json(keymap_json['keymap'], keymap_json['keyboard'], keymap_json['layout'], keymap_json['layers'])
except KeyError:
cli.log.error('Something went wrong. Try to use --no-cpp.')
sys.exit(1)
Expand Down
1 change: 1 addition & 0 deletions lib/python/qmk/cli/generate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import api
58 changes: 58 additions & 0 deletions lib/python/qmk/cli/generate/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""This script automates the generation of the QMK API data.
"""
from pathlib import Path
from shutil import copyfile
from time import strftime
skullydazed marked this conversation as resolved.
Show resolved Hide resolved
import json

from milc import cli

from qmk.info import info_json
from qmk.keyboard import list_keyboards


@cli.subcommand('Creates a new keymap for the keyboard of your choosing', hidden=False if cli.config.user.developer else True)
def generate_api(cli):
"""Generates the QMK API data.
"""
api_data_dir = Path('api_data')
v1_dir = api_data_dir / 'v1'
keyboard_list = v1_dir / 'keyboard_list.json'
keyboard_all = v1_dir / 'keyboards.json'
usb_file = v1_dir / 'usb.json'

if not api_data_dir.exists():
api_data_dir.mkdir()

kb_all = {'last_updated': strftime('%Y-%m-%d %H:%M:%S %Z'), 'keyboards': {}}
usb_list = {'last_updated': strftime('%Y-%m-%d %H:%M:%S %Z'), 'devices': {}}
skullydazed marked this conversation as resolved.
Show resolved Hide resolved

# Generate and write keyboard specific JSON files
for keyboard_name in list_keyboards():
kb_all['keyboards'][keyboard_name] = info_json(keyboard_name)
keyboard_dir = v1_dir / 'keyboards' / keyboard_name
keyboard_info = keyboard_dir / 'info.json'
keyboard_readme = keyboard_dir / 'readme.md'
keyboard_readme_src = Path('keyboards') / keyboard_name / 'readme.md'
skullydazed marked this conversation as resolved.
Show resolved Hide resolved

keyboard_dir.mkdir(parents=True, exist_ok=True)
keyboard_info.write_text(json.dumps(kb_all['keyboards'][keyboard_name]))

if keyboard_readme_src.exists():
copyfile(keyboard_readme_src, keyboard_readme)

if 'usb' in kb_all['keyboards'][keyboard_name]:
usb = kb_all['keyboards'][keyboard_name]['usb']

if usb['vid'] not in usb_list['devices']:
usb_list['devices'][usb['vid']] = {}

if usb['pid'] not in usb_list['devices'][usb['vid']]:
usb_list['devices'][usb['vid']][usb['pid']] = {}

usb_list['devices'][usb['vid']][usb['pid']][keyboard_name] = usb

# Write the global JSON files
keyboard_list.write_text(json.dumps({'last_updated': strftime('%Y-%m-%d %H:%M:%S %Z'), 'keyboards': sorted(kb_all['keyboards'])}))
skullydazed marked this conversation as resolved.
Show resolved Hide resolved
keyboard_all.write_text(json.dumps(kb_all))
usb_file.write_text(json.dumps(usb_list))
56 changes: 28 additions & 28 deletions lib/python/qmk/cli/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
COL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijilmnopqrstuvwxyz'


def show_keymap(info_json, title_caps=True):
def show_keymap(kb_info_json, title_caps=True):
"""Render the keymap in ascii art.
"""
keymap_path = locate_keymap(cli.config.info.keyboard, cli.config.info.keymap)
Expand All @@ -36,7 +36,7 @@ def show_keymap(info_json, title_caps=True):
else:
cli.echo('{fg_cyan}layer_%s{fg_reset}:', layer_num)

print(render_layout(info_json['layouts'][layout_name]['layout'], layer))
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], layer))


def show_layouts(kb_info_json, title_caps=True):
Expand All @@ -48,10 +48,10 @@ def show_layouts(kb_info_json, title_caps=True):
print(layout_art) # Avoid passing dirty data to cli.echo()


def show_matrix(info_json, title_caps=True):
def show_matrix(kb_info_json, title_caps=True):
"""Render the layout with matrix labels in ascii art.
"""
for layout_name, layout in info_json['layouts'].items():
for layout_name, layout in kb_info_json['layouts'].items():
# Build our label list
labels = []
for key in layout['layout']:
Expand All @@ -69,54 +69,54 @@ def show_matrix(info_json, title_caps=True):
else:
cli.echo('{fg_blue}matrix_%s{fg_reset}:', layout_name)

print(render_layout(info_json['layouts'][layout_name]['layout'], labels))
print(render_layout(kb_info_json['layouts'][layout_name]['layout'], labels))


def print_friendly_output(info_json):
def print_friendly_output(kb_info_json):
"""Print the info.json in a friendly text format.
"""
cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', info_json.get('keyboard_name', 'Unknown'))
cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', info_json.get('manufacturer', 'Unknown'))
if 'url' in info_json:
cli.echo('{fg_blue}Website{fg_reset}: %s', info_json.get('url', ''))
if info_json.get('maintainer', 'qmk') == 'qmk':
cli.echo('{fg_blue}Keyboard Name{fg_reset}: %s', kb_info_json.get('keyboard_name', 'Unknown'))
cli.echo('{fg_blue}Manufacturer{fg_reset}: %s', kb_info_json.get('manufacturer', 'Unknown'))
if 'url' in kb_info_json:
cli.echo('{fg_blue}Website{fg_reset}: %s', kb_info_json.get('url', ''))
if kb_info_json.get('maintainer', 'qmk') == 'qmk':
cli.echo('{fg_blue}Maintainer{fg_reset}: QMK Community')
else:
cli.echo('{fg_blue}Maintainer{fg_reset}: %s', info_json['maintainer'])
cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', info_json.get('keyboard_folder', 'Unknown'))
cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys())))
if 'width' in info_json and 'height' in info_json:
cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (info_json['width'], info_json['height']))
cli.echo('{fg_blue}Processor{fg_reset}: %s', info_json.get('processor', 'Unknown'))
cli.echo('{fg_blue}Bootloader{fg_reset}: %s', info_json.get('bootloader', 'Unknown'))
cli.echo('{fg_blue}Maintainer{fg_reset}: %s', kb_info_json['maintainer'])
cli.echo('{fg_blue}Keyboard Folder{fg_reset}: %s', kb_info_json.get('keyboard_folder', 'Unknown'))
cli.echo('{fg_blue}Layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
if 'width' in kb_info_json and 'height' in kb_info_json:
cli.echo('{fg_blue}Size{fg_reset}: %s x %s' % (kb_info_json['width'], kb_info_json['height']))
cli.echo('{fg_blue}Processor{fg_reset}: %s', kb_info_json.get('processor', 'Unknown'))
cli.echo('{fg_blue}Bootloader{fg_reset}: %s', kb_info_json.get('bootloader', 'Unknown'))

if cli.config.info.layouts:
show_layouts(info_json, True)
show_layouts(kb_info_json, True)

if cli.config.info.matrix:
show_matrix(info_json, True)
show_matrix(kb_info_json, True)

if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(info_json, True)
show_keymap(kb_info_json, True)


def print_text_output(info_json):
def print_text_output(kb_info_json):
"""Print the info.json in a plain text format.
"""
for key in sorted(info_json):
for key in sorted(kb_info_json):
if key == 'layouts':
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(info_json['layouts'].keys())))
cli.echo('{fg_blue}layouts{fg_reset}: %s', ', '.join(sorted(kb_info_json['layouts'].keys())))
else:
cli.echo('{fg_blue}%s{fg_reset}: %s', key, info_json[key])
cli.echo('{fg_blue}%s{fg_reset}: %s', key, kb_info_json[key])

if cli.config.info.layouts:
show_layouts(info_json, False)
show_layouts(kb_info_json, False)

if cli.config.info.matrix:
show_matrix(info_json, False)
show_matrix(kb_info_json, False)

if cli.config_source.info.keymap and cli.config_source.info.keymap != 'config_file':
show_keymap(info_json, False)
show_keymap(kb_info_json, False)


@cli.argument('-kb', '--keyboard', help='Keyboard to show info for.')
Expand Down
2 changes: 1 addition & 1 deletion lib/python/qmk/cli/json2c.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def json2c(cli):
user_keymap = json.load(fd)

# Generate the keymap
keymap_c = qmk.keymap.generate(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])
keymap_c = qmk.keymap.generate_c(user_keymap['keyboard'], user_keymap['layout'], user_keymap['layers'])

if cli.args.output:
cli.args.output.parent.mkdir(parents=True, exist_ok=True)
Expand Down
19 changes: 2 additions & 17 deletions lib/python/qmk/cli/list/keyboards.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
"""List the keyboards currently defined within QMK
"""
# We avoid pathlib here because this is performance critical code.
import os
import glob

from milc import cli

BASE_PATH = os.path.join(os.getcwd(), "keyboards") + os.path.sep
KB_WILDCARD = os.path.join(BASE_PATH, "**", "rules.mk")


def find_name(path):
"""Determine the keyboard name by stripping off the base_path and rules.mk.
"""
return path.replace(BASE_PATH, "").replace(os.path.sep + "rules.mk", "")
import qmk.keyboard


@cli.subcommand("List the keyboards currently defined within QMK")
def list_keyboards(cli):
"""List the keyboards currently defined within QMK
"""
# find everywhere we have rules.mk where keymaps isn't in the path
paths = [path for path in glob.iglob(KB_WILDCARD, recursive=True) if 'keymaps' not in path]

# Extract the keyboard name from the path and print it
for keyboard_name in sorted(map(find_name, paths)):
for keyboard_name in qmk.keyboard.list_keyboards():
print(keyboard_name)
8 changes: 8 additions & 0 deletions lib/python/qmk/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from qmk.constants import CHIBIOS_PROCESSORS, LUFA_PROCESSORS, VUSB_PROCESSORS
from qmk.c_parse import find_layouts
from qmk.keyboard import config_h, rules_mk
from qmk.keymap import list_keymaps
from qmk.makefile import parse_rules_mk_file
from qmk.math import compute

Expand All @@ -25,14 +26,21 @@ def info_json(keyboard):
info_data = {
'keyboard_name': str(keyboard),
'keyboard_folder': str(keyboard),
'keymaps': {},
'layouts': {},
'maintainer': 'qmk',
}

# Populate the list of JSON keymaps
for keymap in list_keymaps(keyboard, c=False, fullpath=True):
info_data['keymaps'][keymap.name] = {'url': f'https://raw.githubusercontent.com/qmk/qmk_firmware/master/{keymap}/keymap.json'}

# Populate layout data
for layout_name, layout_json in _find_all_layouts(keyboard, rules).items():
if not layout_name.startswith('LAYOUT_kc'):
info_data['layouts'][layout_name] = layout_json

# Merge in the data from info.json, config.h, and rules.mk
info_data = merge_info_jsons(keyboard, info_data)
info_data = _extract_config_h(info_data)
info_data = _extract_rules_mk(info_data)
Expand Down
20 changes: 20 additions & 0 deletions lib/python/qmk/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
from array import array
from math import ceil
from pathlib import Path
import os
from glob import glob

from qmk.c_parse import parse_config_h_file
from qmk.makefile import parse_rules_mk_file

base_path = os.path.join(os.getcwd(), "keyboards") + os.path.sep


def _find_name(path):
"""Determine the keyboard name by stripping off the base_path and rules.mk.
"""
return path.replace(base_path, "").replace(os.path.sep + "rules.mk", "")


def list_keyboards():
"""Returns a list of all keyboards.
"""
# We avoid pathlib here because this is performance critical code.
kb_wildcard = os.path.join(base_path, "**", "rules.mk")
paths = [path for path in glob(kb_wildcard, recursive=True) if 'keymaps' not in path]

return sorted(map(_find_name, paths))


def config_h(keyboard):
"""Parses all the config.h files for a keyboard.
Expand Down
Loading