From d8c8bb05d9a5c1ab759e8155d10dba3a64139714 Mon Sep 17 00:00:00 2001
From: Trygve Laugstøl <trygvis@inamo.no>
Date: Sun, 8 Jan 2017 00:23:24 +0100
Subject: Starting on a KiCAD ontology. o Supporting many version of a single
 project.

---
 trygvis/eda/cli/__init__.py                     |  7 +-
 trygvis/eda/cli/db_stats.py                     |  2 +-
 trygvis/eda/cli/digikey_download_for_project.py | 15 +---
 trygvis/eda/cli/eda_rdf.py                      | 16 +----
 trygvis/eda/cli/kicad_import_project.py         | 92 ++++++++++++++++++-------
 trygvis/eda/cli/make_bom.py                     | 84 +++++++++++++++++-----
 trygvis/eda/kicad/export/__init__.py            |  2 +-
 trygvis/eda/kicad/rdf.py                        | 13 +++-
 8 files changed, 152 insertions(+), 79 deletions(-)

(limited to 'trygvis/eda')

diff --git a/trygvis/eda/cli/__init__.py b/trygvis/eda/cli/__init__.py
index aa61021..422cf48 100644
--- a/trygvis/eda/cli/__init__.py
+++ b/trygvis/eda/cli/__init__.py
@@ -3,6 +3,7 @@ import logging
 from genericpath import isfile
 from os import mkdir
 from os.path import dirname, isdir
+from typing import Callable
 
 from rdflib import store, ConjunctiveGraph, Graph, RDF, RDFS
 from rdflib.plugins.sparql import prepareQuery
@@ -92,11 +93,11 @@ def create_graph(digikey=False, kicad=False) -> Graph:
 
     if kicad:
         g.bind("kicad", kicad_rdf.KICAD)
-        g.bind("kicad-type", kicad_rdf.KICAD_TYPE)
+        g.bind("kicad-random", kicad_rdf.KICAD_RANDOM)
     return g
 
 
-def write_graph(gen_g: Graph, filename: str = None, force_write: bool = False):
+def write_graph(gen_g: Callable[[], Graph], filename: str = None, force_write: bool = False):
     if filename is not None:
         if force_write or not isfile(filename):
             parent = dirname(filename)
@@ -129,7 +130,7 @@ _initNs = {
     "dk-part": digikey_rdf.DIGIKEY_PART,
     "dk-p-c": digikey_rdf.DIGIKEY_PRODUCT_CATEGORY,
     "kicad": kicad_rdf.KICAD,
-    "kicad-type": kicad_rdf.KICAD_TYPE}
+    "kicad-random": kicad_rdf.KICAD_RANDOM}
 
 
 def sparql(g: Graph, query: str, init_bindings=None):
diff --git a/trygvis/eda/cli/db_stats.py b/trygvis/eda/cli/db_stats.py
index babc53b..430c1f3 100755
--- a/trygvis/eda/cli/db_stats.py
+++ b/trygvis/eda/cli/db_stats.py
@@ -6,7 +6,7 @@ def run(args: object):
         res = cli.sparql(g, """
 SELECT ?project ?label
 WHERE {
-    ?project a kicad-type:project
+    ?project a kicad:project
     OPTIONAL {
         ?project rdfs:label ?label
     }
diff --git a/trygvis/eda/cli/digikey_download_for_project.py b/trygvis/eda/cli/digikey_download_for_project.py
index 02e3cb1..a7d93e7 100755
--- a/trygvis/eda/cli/digikey_download_for_project.py
+++ b/trygvis/eda/cli/digikey_download_for_project.py
@@ -24,20 +24,7 @@ def work(project_url, force, g):
     db = DigikeyDatabase()
     download_category_tree(db, client)
 
-    # Dump project:
-    # SELECT ?project
-    # WHERE {
-    #  ?project a kicad-type:project .
-    #  b:rateboard-2_a ? ?.
-    #  ?s ?predicate ?object .
-    # }
-
-    # allComponentsQ = prepareQuery('SELECT ?ref ?value WHERE { ?project_url a kicad-type:project .  ?project_url kicad:component ?o . ?o rdfs:label ?ref .  ?o kicad:value ?value . }', initNs = initNs)
-    #
-    # for row in g.query(allComponentsQ, initBindings={'project_url': project_url}):
-    #    print("ref=%s, value=%s" % (row.ref, row.value))
-
-    res = cli.sparql(g, "SELECT ?project WHERE {?project a kicad-type:project}")
+    res = cli.sparql(g, "SELECT ?project WHERE {?project a kicad:project}")
     print("Found %d projects in database" % len(res))
     for row in res:
         print("project: %s" % row.project)
diff --git a/trygvis/eda/cli/eda_rdf.py b/trygvis/eda/cli/eda_rdf.py
index 0df66ac..ff11fda 100644
--- a/trygvis/eda/cli/eda_rdf.py
+++ b/trygvis/eda/cli/eda_rdf.py
@@ -1,6 +1,7 @@
 from trygvis.eda.cli.digikey_download_for_project import DigikeyDownloadForProjectCommand
 from trygvis.eda.cli.init import InitCommand
 from trygvis.eda.cli.kicad_import_project import KicadImportProjectCommand
+from trygvis.eda.cli.make_bom import MakeBomCommand
 from . import *
 
 
@@ -61,19 +62,6 @@ class DigikeyDownloadAttributeTypesForCategory(CliCommand):
         digikey_download_attribute_types_for_category.run(args.category, args.sub_category, args.output, args)
 
 
-class MakeBom(CliCommand):
-    def __init__(self):
-        super().__init__("make-bom", "Create a BOM for a project with all info for each part.")
-
-    def run(self, argv):
-        p = argparse.ArgumentParser(prog=self.key, description=self.description)
-        p.add_argument("--schematic", required=True)
-        args = p.parse_args(argv)
-
-        from trygvis.eda.cli import make_bom
-        make_bom.run(args.schematic)
-
-
 def main():
     initialize()
 
@@ -81,7 +69,7 @@ def main():
         AddToDb(),
         InitCommand(),
         DbStats(),
-        MakeBom(),
+        MakeBomCommand(),
         KicadImportProjectCommand(),
         DigikeyDownloadForProjectCommand(),
         DigikeyDownloadAttributeTypesForCategory(),
diff --git a/trygvis/eda/cli/kicad_import_project.py b/trygvis/eda/cli/kicad_import_project.py
index 288b7fc..422f24a 100755
--- a/trygvis/eda/cli/kicad_import_project.py
+++ b/trygvis/eda/cli/kicad_import_project.py
@@ -1,10 +1,11 @@
 import os.path
 from operator import attrgetter
 import itertools
+from uuid import uuid4, UUID
+import datetime
 
 import rdflib
 from rdflib import Literal, URIRef
-from rdflib.namespace import RDF, RDFS
 
 # from ..kicad import rdf as kicad_rdf
 from . import *
@@ -27,6 +28,9 @@ class KicadImportProjectCommand(CliCommand):
 def run(args):
     config = read_config()
 
+    version = None
+    timestamp = None  # type: datetime.datetime
+
     if args.input is "-":
         src = sys.stdin
     else:
@@ -40,30 +44,49 @@ def run(args):
             raise CliException("No such file: %s. Did you export the BOM?" % filename)
 
         src = open(filename, 'r')
+        s = os.stat(filename)
+        timestamp = datetime.datetime.fromtimestamp(s.st_mtime)
 
     project_url = config['project']['url']
 
+    if version is None:
+        if timestamp is None:
+            version = uuid4()
+            timestamp = datetime.datetime.now()
+        else:
+            from random import Random
+            r = Random(timestamp.year + timestamp.month + timestamp.day + timestamp.hour + timestamp.minute +
+                       timestamp.second + timestamp.microsecond)
+            s = ["0123456789abcdef"[int(r.random() * 16)] for _ in range(0, 32)]
+            version = UUID(''.join(s))
+
+    info("Version: %s, timestamp=%s" % (version, timestamp))
+
     with src:
-        project = export_to_graph(src, project_url)
+        (version, project_g) = export_to_graph(src, project_url, version, timestamp)
 
-    debug('Loaded %s tuples' % len(project))
+    debug('Loaded %s tuples' % len(project_g))
+
+    project_g.add((URIRef(project_url), RDF.type, kicad_rdf.KICAD['project']))
+    project_g.add((URIRef(project_url), kicad_rdf.KICAD['version'], version))
 
     if args.output_dir is not None:
         parent = args.output_dir
         if not os.path.exists(parent):
             os.mkdir(parent)
-        output_file = os.path.join(parent, "project.ttl")
-        with open(output_file, 'wb') as dst:
-            project.serialize(destination=dst, encoding='utf-8', format='turtle')
+
+        v = timestamp.strftime("%Y-%m-%d %H:%M:%S")
+        output_file = os.path.join(parent, "project-%s.ttl" % v)
+        write_graph(lambda: project_g, filename=output_file, force_write=True)
 
     def import_project(g):
-        for idx, t in enumerate(project.triples((None, None, None))):
+        for idx, t in enumerate(project_g.triples((None, None, None))):
             g.add(t)
 
     with_database(import_project)
 
 
-def export_to_graph(src: object, project_url: str) -> rdflib.Graph:
+def export_to_graph(src: object, project_url: str, version: str, timestamp: datetime.datetime) -> (str, rdflib.Graph):
     export = Export.from_xml_file(src)
 
     # print('components:')
@@ -135,8 +158,11 @@ def export_to_graph(src: object, project_url: str) -> rdflib.Graph:
 
     g = create_graph(kicad=True)
 
-    project = URIRef(project_url)
-    g.add((project, RDF.type, kicad_rdf.KICAD_TYPE.project))
+    version_uri = URIRef('urn:uuid:%s#' % version)
+    g.add((version_uri, RDF.type, kicad_rdf.KICAD["project-version"]))
+    g.add((version_uri, RDFS.label, Literal('Version: %s' % timestamp.strftime("%Y-%m-%d %H:%M:%S"))))
+    g.add((version_uri, kicad_rdf.KICAD['version-of'], URIRef(project_url)))
+    g.add((version_uri, kicad_rdf.KICAD['timestamp'], Literal(timestamp)))
 
     # The components and fields could/should have been a BNodes. Their generated names are not very nice.
     # TODO: try using a hash of the current value and put that under a special generated namespace.
@@ -145,29 +171,43 @@ def export_to_graph(src: object, project_url: str) -> rdflib.Graph:
     # add_prefix(hash) => 'https://trygvis.io/purl/kicad/generated#123456'
     footprints = set()
     for c in export.components:
-        ns = rdflib.Namespace(project_url)
+        ns = rdflib.Namespace(str(version_uri))
         node = ns[c.ref]
-        g.add((project, kicad_rdf.KICAD.component, node))
+        g.add((version_uri, kicad_rdf.KICAD['component'], node))
 
-        g.add((node, RDF.type, kicad_rdf.KICAD_TYPE.schematic_component))
+        g.add((node, RDF.type, kicad_rdf.KICAD['component']))
         g.add((node, RDFS.label, Literal(c.ref)))
-        g.add((node, kicad_rdf.KICAD.value, Literal(c.value.text)))
+        g.add((node, kicad_rdf.KICAD['value'], Literal(c.value.text)))
 
         footprint_uri = URIRef(kicad_rdf.KICAD_FOOTPRINT[quote_plus(c.footprint)])
-        if not footprint_uri in footprints:
-            g.add((footprint_uri, RDF.type, kicad_rdf.KICAD_TYPE.footprint))
+        if footprint_uri not in footprints:
+            g.add((footprint_uri, RDF.type, kicad_rdf.KICAD['footprint']))
             g.add((footprint_uri, RDFS.label, Literal(c.footprint)))
             footprints.add(footprint_uri)
 
-        g.add((node, kicad_rdf.KICAD.footprint, footprint_uri))
+        g.add((node, kicad_rdf.KICAD['footprint'], footprint_uri))
 
         for name, value in c.fields.items():
-            f = ns['%s-%s' % (c.ref, name)]
-            g.add((node, kicad_rdf.KICAD.field, f))
-            g.add((f, RDF.type, kicad_rdf.KICAD_TYPE.field))
-            g.add((f, kicad_rdf.KICAD.field_name, Literal(name)))
-            g.add((f, kicad_rdf.KICAD.field_value, Literal(value)))
-
-    # TODO: serialize the data from <design> too
-
-    return g
+            # f = ns['%s-%s' % (c.ref, name)]
+            f = kicad_rdf.gen_random('%s-%s' % (c.ref, name))
+            g.add((node, kicad_rdf.KICAD['field'], f))
+            g.add((f, RDF.type, kicad_rdf.KICAD['field']))
+            g.add((f, kicad_rdf.KICAD['field-name'], Literal(name)))
+            g.add((f, kicad_rdf.KICAD['field-value'], Literal(value)))
+
+    d = export.design
+    if d is not None:
+        g.add((version_uri, kicad_rdf.KICAD['design-date'], Literal(d.date)))
+
+        for s in d.sheets:
+            sheet_uri = kicad_rdf.gen_random(s.number)
+            g.add((sheet_uri, RDF.type, kicad_rdf.KICAD['sheet']))
+            label = s.title_block.title if s.title_block.title is not None and len(s.title_block.title) > 0 else s.name
+            g.add((sheet_uri, RDFS.label, Literal(label)))
+            g.add((sheet_uri, kicad_rdf.KICAD['sheet-name'], Literal(s.name)))
+            g.add((sheet_uri, kicad_rdf.KICAD['sheet-number'], Literal(int(s.number))))
+            g.add((sheet_uri, kicad_rdf.KICAD['title'], Literal(s.title_block.title)))
+            g.add((sheet_uri, kicad_rdf.KICAD['rev'], Literal(s.title_block.rev)))
+            g.add((sheet_uri, kicad_rdf.KICAD['date'], Literal(s.title_block.date)))
+
+    return version_uri, g
diff --git a/trygvis/eda/cli/make_bom.py b/trygvis/eda/cli/make_bom.py
index a47e04a..1023c11 100755
--- a/trygvis/eda/cli/make_bom.py
+++ b/trygvis/eda/cli/make_bom.py
@@ -1,7 +1,22 @@
-from trygvis.eda import cli
-import trygvis.eda.digikey.rdf as digikey_rdf
+import argparse
 import rdflib
 
+import trygvis.eda.digikey.rdf as digikey_rdf
+from trygvis.eda import cli, EdaException
+
+
+class MakeBomCommand(cli.CliCommand):
+    def __init__(self):
+        super().__init__("make-bom", "Create a BOM for a project with all info for each part.")
+
+    def run(self, argv):
+        p = argparse.ArgumentParser(prog=self.key, description=self.description)
+        p.add_argument("--project")
+        p.add_argument("--version")
+        args = p.parse_args(argv)
+
+        run(args)
+
 
 class DigiKeyPart(object):
     def __init__(self, part_number):
@@ -22,49 +37,80 @@ class Component(object):
         self.fields[field_name] = field_value
 
 
-def run(project_url):
-    cli.with_database(lambda g: work(project_url, g))
+def run(args):
+    cli.with_database(lambda g: work(args, g))
 
 
-def work(project_url, g):
+def work(args, g):
+    config = cli.read_config()
+    project_url = args.project if args.project is not None else config['project']['url']
+
     components = {}
     dk_parts = {}
 
-    cli.info("Loading components")
+    version = None
+    if args.version is not None:
+        version = args.version
+    else:
+        cli.info("Finding latest version")
+        res = cli.sparql(g, """
+SELECT
+?project ?version ?timestamp
+WHERE {
+    ?project a kicad:project .
+  ?version  		a kicad:project-version;
+              kicad:timestamp ?timestamp;
+              kicad:version-of ?project
+  .
+}
+ORDER BY DESC(?timestamp)
+LIMIT 1
+""", init_bindings={"project": rdflib.URIRef(project_url)})
+
+        for row in res:
+            version = row.version
+            break
+        else:
+            raise EdaException("Could not find any version of project %s" % project_url)
+
+    cli.info("Loading components for version %s" % version)
 
     res = cli.sparql(g, """
 SELECT
     ?ref ?value
 WHERE {
-    ?project a kicad-type:project ;
+    ?version a kicad:project-version ;
         kicad:component ?cmp .
-  ?cmp a kicad-type:schematic_component ;
-      kicad:value ?value ;
-      rdfs:label ?ref .
+    ?cmp a kicad:component ;
+        kicad:value ?value ;
+        rdfs:label ?ref .
 }
 ORDER BY ?ref
-""", init_bindings={"project": rdflib.URIRef(project_url)})
+""", init_bindings={"version": rdflib.URIRef(version)})
 
     for row in res:
         c = Component(row.ref, row.value)
         components[row.ref] = c
         cli.info('ref=%s, value=%s' % (c.ref, c.value))
 
+    if len(components) == 0:
+        raise EdaException("Could not find any components")
+
     cli.info("Loading custom component attributes")
     res = cli.sparql(g, """
 SELECT
     ?ref ?field ?field_name ?field_value
 WHERE {
-    ?project a kicad-type:project ;
+    ?version a kicad:project-version ;
         kicad:component ?cmp .
-    ?cmp a kicad-type:schematic_component ;
+    ?cmp a kicad:component ;
         rdfs:label ?ref ;
         kicad:field ?field .
-    ?field a kicad-type:field ; kicad:field_name ?field_name .
-    ?field a kicad-type:field ; kicad:field_value ?field_value .
+    ?field a kicad:field ; kicad:field_name ?field_name .
+    ?field a kicad:field ; kicad:field_value ?field_value .
 }
 ORDER BY ?ref ?field_name
-""", init_bindings={"project": rdflib.URIRef(project_url)})
+""", init_bindings={"version": rdflib.URIRef(version)})
 
     for row in res:
         c = components[row.ref]
@@ -78,7 +124,7 @@ SELECT
 ?ref ?part_number ?type ?value ?attr_type
 WHERE {
   ?project kicad:component ?cmp .
-  ?cmp a kicad-type:schematic_component ;
+  ?cmp a kicad:component ;
        rdfs:label ?ref ;
        kicad:field ?d .
   ?d kicad:field_name "digikey" ;
@@ -114,7 +160,7 @@ SELECT
 # *
 WHERE {
   ?project kicad:component ?cmp .
-  ?cmp a kicad-type:schematic_component ;
+  ?cmp a kicad:component ;
        rdfs:label ?ref ;
        kicad:field ?dk_field .
   optional {
@@ -138,7 +184,7 @@ WHERE {
 #    ?case_type a dk:attributeType ; dk:value ?case_url .
 #  } .
 
-#  VALUES (?project_url) { (<https://trygvis/purl/kicad-board#SweetZpot+Sensor>) } .
+#  VALUES (?project_url) { (...) } .
 #  VALUES (?package_type) { (dk-attr-type:pv16) } .
 #  VALUES (?case_type) { (dk-attr-type:pv1291) } .
 }
diff --git a/trygvis/eda/kicad/export/__init__.py b/trygvis/eda/kicad/export/__init__.py
index 64b82d2..9f3e96e 100644
--- a/trygvis/eda/kicad/export/__init__.py
+++ b/trygvis/eda/kicad/export/__init__.py
@@ -174,7 +174,7 @@ class Sheet(object):
     def __init__(self, number, name, title_block):
         self.number = number
         self.name = name
-        self.title_block = title_block
+        self.title_block = title_block  # type: TitleBlock
 
     @staticmethod
     def from_xml(s):
diff --git a/trygvis/eda/kicad/rdf.py b/trygvis/eda/kicad/rdf.py
index 34b73e1..2984fcd 100644
--- a/trygvis/eda/kicad/rdf.py
+++ b/trygvis/eda/kicad/rdf.py
@@ -1,8 +1,19 @@
 import rdflib
+from trygvis.eda import EdaException
 
 KICAD = rdflib.Namespace("https://trygvis/purl/kicad#")
-KICAD_TYPE = rdflib.Namespace("https://trygvis/purl/kicad-type#")
+KICAD_RANDOM = rdflib.Namespace("https://trygvis/purl/kicad-random/")
 KICAD_FOOTPRINT = rdflib.Namespace("https://trygvis/purl/kicad-footprints#")
 
 # Namespace for all unknown kicad boards
 KICAD_BOARD = rdflib.Namespace("https://trygvis/purl/kicad-board#")
+
+
+def gen_random(key: object):
+    s = str(key)
+
+    if len(s) == 0:
+        raise EdaException("Can't generate a random URI for empty strings.")
+
+    s = str(abs(hash(s)))
+    return KICAD_RANDOM.term(s)
-- 
cgit v1.2.3