PKL']0]0layouts/__init__.py''' Python API for HID-IO HID Layouts Repository - Allows querying from a GitHub cache (or internal cache) - Handles JSON merging - Can query all possible layouts TODO - pip package - Initial local cache - Retrieving cache Call using python virtual env + python -m layouts 1) cd layouts.git 2) pipenv install 3) pipenv shell 4) python -m layouts --help ''' ## Imports import argparse import glob import json import logging import os import shutil import tarfile import tempfile import requests from github import Github ## Variables __version__ = '0.1' log = logging.getLogger(__name__) ## Classes class Layouts: ''' Retrieves various HID layouts using either an internal (or external) cache. If there is no internal cache defined, one is downloaded from GitHub. By default, when retrieving a cache from GitHub, the latest version is used. ''' def __init__(self, github_path='hid-io/layouts', version='master', force_refresh=False, cache_dir=tempfile.gettempdir(), token=None ): ''' TODO - Local cache @param github_path: Location of the git repo on GitHub (e.g. hid-io/layouts) @param version: git reference for the version to download (e.g. master) @param force_refresh: If True, always check GitHub for the latest cache @param cache_dir: Directory to operate on external cache from @param token: GitHub access token, defaults to None ''' # Check to see if there is a cache available already match = "*{}*".format(github_path.replace('/', '-')) matches = sorted(glob.glob(os.path.join(cache_dir, match))) # If force refresh, clear cache, and return no matches if force_refresh: for mat in matches: shutil.rmtree(mat) matches = [] # No cache, retrieve from GitHub if not matches: self.retrieve_github_cache(github_path, version, cache_dir, token) matches = sorted(glob.glob(os.path.join(cache_dir, match))) # Select the newest cache self.layout_path = matches[-1] # Scan for all JSON files self.json_file_paths = sorted(glob.glob(os.path.join(self.layout_path, "**/*.json"))) # Load each of the JSON files into memory self.json_files = {} for path in self.json_file_paths: with open(path) as json_file: self.json_files[path] = json.load(json_file) # Query names for all of the layouts self.layout_names = {} for json_file, json_data in self.json_files.items(): for name in json_data['name']: self.layout_names[name] = json_file def retrieve_github_cache(self, github_path, version, cache_dir, token): ''' Retrieves a cache of the layouts git repo from GitHub @param github_path: Location of the git repo on GitHub (e.g. hid-io/layouts) @param version: git reference for the version to download (e.g. master) @param cache_dir: Directory to operate on external cache from @param token: GitHub access token ''' # Check for environment variable Github token token = os.environ.get('GITHUB_APIKEY', None) # Retrieve repo information gh = Github(token) repo = gh.get_repo(github_path) commit = repo.get_commit(version) commits = repo.get_commits() total_commits = 0 commit_number = 0 for cmt in commits: if commit == cmt: commit_number = total_commits total_commits += 1 commit_number = total_commits - commit_number tar_url = repo.get_archive_link('tarball', commit.sha) # GitHub only uses the first 7 characters of the sha in the download dirname_orig = "{}-{}".format(github_path.replace('/', '-'), commit.sha[:7]) dirname_orig_path = os.path.join(cache_dir, dirname_orig) # Adding a commit number so it's clear which is the latest version without requiring git dirname = "{}-{}".format(commit_number, dirname_orig) dirname_path = os.path.join(cache_dir, dirname) # If directory doesn't exist, check if tarball does if not os.path.isdir(dirname_path): filename = "{}.tar.gz".format(dirname) filepath = os.path.join(cache_dir, filename) # If tarball doesn't exist, download it if not os.path.isfile(filepath): # Retrieve tar file chunk_size = 2000 req = requests.get(tar_url, stream=True) with open(filepath, 'wb') as infile: for chunk in req.iter_content(chunk_size): infile.write(chunk) # Extract tarfile tar = tarfile.open(filepath) tar.extractall(cache_dir) os.rename(dirname_orig_path, dirname_path) # Remove tar.gz os.remove(filepath) def list_layouts(self): ''' Returns a list of all defined names/aliases for HID layouts ''' return sorted(list(self.layout_names.keys())) def get_layout(self, name): ''' Returns the layout with the given name ''' layout_chain = [] # Retrieve initial layout file json_data = self.json_files[self.layout_names[name]] layout_chain.append(Layout(name, json_data)) # Recursively locate parent layout files parent = layout_chain[-1].parent() while parent is not None: # Find the parent parent_path = None for path in self.json_file_paths: if parent in path: parent_path = path # Make sure a path was found if parent_path is None: raise UnknownLayoutPathException('Could not find: {}'.format(parent_path)) # Build layout for parent json_data = self.json_files[parent_path] layout_chain.append(Layout(parent_path, json_data)) # Check parent of parent parent = layout_chain[-1].parent() # Squash layout files layout = self.squash_layouts(layout_chain) return layout def dict_merge(self, merge_to, merge_in): ''' Recursively merges two dicts Overwrites any non-dictionary items merge_to <- merge_in Modifies merge_to dictionary @param merge_to: Base dictionary to merge into @param merge_in: Dictionary that may overwrite elements in merge_in ''' for key, value in merge_in.items(): # Just add, if the key doesn't exist yet # Or if set to None/Null if key not in merge_to.keys() or merge_to[key] is None: merge_to[key] = value continue # Overwrite case, check for types # Make sure types are matching if not isinstance(value, type(merge_to[key])): raise MergeException('Types do not match! {}: {} != {}'.format(key, type(value), type(merge_to[key]))) # Check if this is a dictionary item, in which case recursively merge if isinstance(value, dict): self.dict_merge(merge_to[key], value) continue # Otherwise just overwrite merge_to[key] = value def squash_layouts(self, layouts): ''' Returns a squashed layout The first element takes precedence (i.e. left to right). Dictionaries are recursively merged, overwrites only occur on non-dictionary entries. [0,1] 0: test: 'my data' 1: test: 'stuff' Result: test: 'my data' @param layouts: List of layouts to merge together @return: New layout with list of layouts squash merged ''' top_layout = layouts[0] json_data = {} # Generate a new container Layout layout = Layout(top_layout.name(), json_data, layouts) # Merge in each of the layouts for mlayout in reversed(layouts): # Overwrite all fields, *except* dictionaries # For dictionaries, keep recursing until non-dictionaries are found self.dict_merge(layout.json(), mlayout.json()) return layout class UnknownLayoutPathException(Exception): ''' Thrown when an unknown layout path is used ''' pass class ComposeException(Exception): ''' Thrown when a Layout composition is not possible with a given layout. ''' pass class MergeException(Exception): ''' Thrown when an unexpected merge situation arises. Usually when the item types differ. ''' pass class Layout: ''' Container class for each JSON layout dictionary. Includes some convenience functions that are useful with composition. ''' def __init__(self, name, json_data, parents=None): ''' @param name: Name used to define layout @param json_data: JSON data for layout @param parents: List of parent Layout objects when doing a squash merge ''' self.layout_name = name self.json_data = json_data self.json_data_orig = json_data self.parents = parents def name(self): ''' Name attributed to the layout initially To get all possible names, use self.json_data['name'] instead. @return: Attributed name for the layout ''' return self.layout_name def json(self): ''' Returns a JSON dictionary for the layout @return: JSON data for layout ''' return self.json_data def json_orig(self): ''' Returns the original JSON dictionary for the layout (exludes squashing) @return: Original JSON data for layout ''' return self.json_data_orig def __repr__(self): ''' String representation of Layout ''' return "Layout(name={})".format(self.layout_name) def parent(self): ''' Returns the parent file of the layout @returns: Parent file of layout, None if there is none. ''' return self.json_data['parent'] def compose(self, text): ''' Returns the sequence of combinations necessary to compose given text. If the text expression is not possible with the given layout an ComposeException is thrown. Iterate over the string, converting each character into a key sequence. Between each character, an empty combo is inserted to handle duplicate strings (and USB HID codes between characters) TODO (HaaTa): Add intelligence to not include an empty combo when it is certain there won't be conflicts. ''' sequence = [] for char in text: # Make sure the composition element is available if char not in self.json_data['composition']: raise ComposeException("'{}' is not defined as a composition in the layout '{}'".format(char, self.name)) # Lookup the sequence to handle this character sequence.extend(self.json_data['composition'][char]) # Add empty combo for sequence splitting # TODO (HaaTa): Add intelligence to know when to not add an empty combo sequence.extend([[]]) return sequence ## Functions def main(argv=None): ''' Main entry-point for calling layouts directly as a program. ''' # Prep argparse ap = argparse.ArgumentParser( description='Basic query options for Python HID-IO Layouts repository', ) ap.add_argument('--list', action='store_true', help='List available layout aliases.') ap.add_argument('--get', metavar='NAME', help='Retrieve the given layout, and return the JSON data') # Parse arguments args = ap.parse_args(argv) # Create layouts context manager mgr = Layouts() # Check if generating a list if args.list: for name in mgr.list_layouts(): print(name) # Retrieve JSON layout if args.get is not None: layout = mgr.get_layout(args.get) print(json.dumps(layout.json())) PKLttlayouts/__main__.py''' Main entry-point for python -m layouts ''' from __future__ import absolute_import from . import main main() PK L@`77layouts-0.1.dist-info/LICENSECopyright (c) 2018 Jacob Alexander 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. PK!HNOlayouts-0.1.dist-info/WHEEL HM K-*ϳR03rOK-J,/RH,zd&Y)r$[)T&UrPK!Hwh6layouts-0.1.dist-info/METADATATQO0~ڇ)*2h-a{8n&M;w%z.WN*wlKLAFޱ]-VuYrۤpBW0V[8ϒ2<ࢫk4Imt9N&EgR$RO^bZfv!3U,, 6)nS8T3OR?||r 5Et-h|f2+%Xd't*vsr+&P !Ma!X05G*|ݧJ-/KYbXG~Bx[{)2!~!7[˥BbL:#:G4ݤCz ~W#bX}5QG?-E8I+I h\1Kh1.n+SWs*|&<*R0zPF4lf ia(LKnmGwga+#ʡ%i;a-v4OVeOSo;j[ⳄF)PKI$h.>5 BܛєPKL']0]0layouts/__init__.pyPKLtt0layouts/__main__.pyPK L@`7731layouts-0.1.dist-info/LICENSEPK!HNO5layouts-0.1.dist-info/WHEELPK!Hwh6,6layouts-0.1.dist-info/METADATAPK!HK%8layouts-0.1.dist-info/RECORDPKG: