Commits

Kirill Simonov  committed 30fe0bc

Added `tweak.gateway` addon: create gateways to other databases.

  • Participants
  • Parent commits 10d7698

Comments (0)

Files changed (8)

         'tweak.csrf = htsql.tweak.csrf:TweakCSRFAddon',
         'tweak.django = htsql.tweak.django:TweakDjangoAddon',
         'tweak.etl = htsql.tweak.etl:TweakETLAddon',
+        'tweak.gateway = htsql.tweak.gateway:TweakGatewayAddon',
         'tweak.hello = htsql.tweak.hello:TweakHelloAddon',
         'tweak.inet = htsql.tweak.inet:TweakINetAddon',
         'tweak.inet.pgsql = htsql_pgsql.tweak.inet:TweakINetPGSQLAddon',

File src/htsql/core/classify.py

 #
 
 
+from .util import to_name
 from .context import context
 from .cache import once
 from .adapter import Adapter, adapt
                     ColumnArc, SyntaxArc, AmbiguousArc)
 from .entity import DirectJoin, ReverseJoin
 from .introspect import introspect
-import re
-import unicodedata
 
 
-def normalize(name):
-    """
-    Normalizes a name to provide a valid HTSQL identifier.
-
-    We assume `name` is a Unicode string.  Then it is:
-
-    - translated to Unicode normal form C;
-    - converted to lowercase;
-    - has non-alphanumeric characters replaced with underscores;
-    - preceded with an underscore if it starts with a digit.
-
-    The result is a valid HTSQL identifier.
-    """
-    assert isinstance(name, unicode) and len(name) > 0
-    name = unicodedata.normalize('NFC', name).lower()
-    name = re.sub(ur"(?u)^(?=\d)|\W", u"_", name)
-    return name
+normalize = to_name
 
 
 class Classify(Adapter):

File src/htsql/core/util.py

 import datetime, time
 import keyword
 import operator
+import unicodedata
 
 
 #
 
 
 #
+# Name normalization.
+#
+
+
+def to_name(value):
+    """
+    Converts a non-empty string to a valid HTSQL identifier.
+
+    The given value is transformed as follows:
+
+    - translated to Unicode normal form C;
+    - converted to lowercase;
+    - has non-alphanumeric characters replaced with underscores;
+    - preceded with an underscore if it starts with a digit.
+    """
+    assert isinstance(value, unicode) and len(value) > 0
+    value = unicodedata.normalize('NFC', value).lower()
+    value = re.sub(ur"(?u)^(?=\d)|\W", u"_", value)
+    return value
+
+
+#
 # Topological sorting.
 #
 

File src/htsql/core/validator.py

 """
 
 
-from util import DB, maybe, oneof, listof, tupleof, dictof
+from util import DB, maybe, oneof, listof, tupleof, dictof, to_name
 import re
 
 
         return value
 
 
+class NameVal(Validator):
+    """
+    Verifies if the value is a valid HTSQL identifier.
+
+    `is_nullable` (Boolean)
+        If set, ``None`` values are permitted.
+    """
+
+    hint = """a name"""
+
+    # A regular expression for matching words.
+    regexp = re.compile(r'^(?!\d)\w+$', re.U)
+
+    def __init__(self, is_nullable=False):
+        # Sanity check on the arguments.
+        assert isinstance(is_nullable, bool)
+        self.is_nullable = is_nullable
+
+    def __call__(self, value):
+        # `None` is allowed if the `is_nullable` flag is set.
+        if value is None:
+            if self.is_nullable:
+                return None
+            else:
+                raise ValueError("the null value is not permitted")
+
+        # A byte or a Unicode string is expected.
+        if not isinstance(value, (str, unicode)):
+            raise ValueError("a string value is expected; got %r" % value)
+
+        # Byte strings must be UTF-8 encoded.
+        if isinstance(value, str):
+            try:
+                value = value.decode('utf-8')
+            except UnicodeDecodeError, exc:
+                raise ValueError("unable to decode %r: %s" % (value, exc))
+
+        # Check if the string matches the name pattern.
+        if not self.regexp.match(value):
+            raise ValueError("a string containing alphanumeric characters"
+                             " and underscores is expected;"
+                             " got %r" % value)
+
+        # Normalize and return the value.
+        return to_name(value)
+
+
 class ChoiceVal(Validator):
     """
     Verifies if the value belongs to a specified set of string constants.

File src/htsql/tweak/gateway/__init__.py

+#
+# Copyright (c) 2006-2012, Prometheus Research, LLC
+#
+
+
+from . import command
+from ...core.addon import Addon, Parameter
+from ...core.validator import MapVal, NameVal, DBVal
+from .command import BindGateway
+
+
+class TweakGatewayAddon(Addon):
+
+    name = 'tweak.gateway'
+    hint = """define gateways to other databases"""
+    help = """
+    This addon allows you to create a gateway to another database
+    and execute HTSQL queries against it.
+
+    Parameter `gateways` is a mapping of names to connection URIs.
+    Each mapping entry creates a function which takes a query
+    as a parameter and execute it against the database specified
+    by the connection URI.
+    """
+
+    parameters = [
+            Parameter('gateways', MapVal(NameVal(), DBVal()),
+                      default={},
+                      value_name="{NAME:DB}",
+                      hint="""gateway definitions"""),
+    ]
+
+    def __init__(self, app, attributes):
+        super(TweakGatewayAddon, self).__init__(app, attributes)
+        self.functions = {}
+        for name in sorted(self.gateways):
+            db = self.gateways[name]
+            instance = app.__class__(db)
+            class_name = "Bind%s" % name.title().replace('_', '').encode('utf-8')
+            namespace = {
+                '__names__': [(name.encode('utf-8'), 1)],
+                'instance': instance,
+            }
+            bind_class = type(class_name, (BindGateway,), namespace)
+            self.functions[name] = bind_class
+
+

File src/htsql/tweak/gateway/command.py

+#
+# Copyright (c) 2006-2012, Prometheus Research, LLC
+#
+
+
+from ...core.context import context
+from ...core.adapter import adapt, call
+from ...core.cmd.command import ProducerCmd, DefaultCmd
+from ...core.cmd.act import Act, ProduceAction, act
+from ...core.tr.fn.bind import BindCommand
+from ...core.tr.signature import UnarySig
+from ...core.tr.bind import bind
+from ...core.tr.syntax import QuerySyntax, SegmentSyntax
+from ...core.tr.binding import CommandBinding
+from ...core.tr.error import BindError
+from ...core.tr.lookup import lookup_command
+
+
+class GatewayCmd(ProducerCmd):
+
+    def __init__(self, instance, syntax, environment=None):
+        self.instance = instance
+        self.syntax = syntax
+        self.environment = environment
+
+
+class BindGateway(BindCommand):
+
+    signature = UnarySig
+    instance = None
+
+    def expand(self, op):
+        if not isinstance(op, SegmentSyntax):
+            raise BindError("a segment is required", op.mark)
+        op = QuerySyntax(op, op.mark)
+        command = GatewayCmd(self.instance, op,
+                             environment=self.state.environment)
+        return CommandBinding(self.state.scope, command, self.syntax)
+
+
+class ProduceGateway(Act):
+
+    adapt(GatewayCmd, ProduceAction)
+
+    def __call__(self):
+        can_read = context.env.can_read
+        can_write = context.env.can_write
+        with self.command.instance:
+            with context.env(can_read=context.env.can_read and can_read,
+                             can_write=context.env.can_write and can_write):
+                binding = bind(self.command.syntax,
+                               environment=self.command.environment)
+                command = lookup_command(binding)
+                if command is None:
+                    command = DefaultCmd(binding)
+                product = act(command, self.action)
+        return product
+
+

File test/input/addon.yaml

               del sys.modules[name]
       del __builtin__.sandbox
 
+# TWEAK.GATEWAY - define gateways to other databases
+- title: tweak.gateway
+  ifdef: mysql
+  tests:
+  # Addon description
+  - ctl: [ext, tweak.gateway]
+
+  # Load the addon
+  - load: demo
+    extensions:
+      tweak.gateway:
+        gateways:
+          sqlite_gw: sqlite:///build/regress/sqlite/htsql_demo.sqlite
+          pgsql_gw:
+            engine: pgsql
+            database: htsql_demo
+            username: htsql_demo
+            password: secret
+            host: ${PGSQL_HOST}
+            port: ${PGSQL_PORT}
+
+  # Normal queries
+  - uri: /sqlite_gw(/school)
+  - uri: /department[ee]{name, count(course)}/:pgsql_gw
+
+  # Formats and commands
+  - uri: /sqlite_gw(/school[art].program)/:json
+  - uri: /pgsql_gw(/fetch(count(course)))/:raw
+
+  # Errors
+  - uri: /pgsql_gw(/school/:html)
+    expect: 400
+  - uri: /sqlite_gw(/nothing)
+    expect: 400
+
 # TWEAK.INET - IPv4 data type
 - title: tweak.inet
   ifdef: pgsql

File test/output/mysql.yaml

              ORDER BY `polls_choice`.`id` ASC
         - py: remove-module-path
           stdout: ''
+      - id: tweak.gateway
+        tests:
+        - ctl: [ext, tweak.gateway]
+          stdout: |+
+            TWEAK.GATEWAY - define gateways to other databases
+
+            This addon allows you to create a gateway to another database
+            and execute HTSQL queries against it.
+
+            Parameter `gateways` is a mapping of names to connection URIs.
+            Each mapping entry creates a function which takes a query
+            as a parameter and execute it against the database specified
+            by the connection URI.
+
+            Parameters:
+              gateways={NAME:DB}       : gateway definitions
+
+          exit: 0
+        - uri: /sqlite_gw(/school)
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          - [Vary, Accept]
+          body: |2
+             | school                                        |
+             +------+-------------------------------+--------+
+             | code | name                          | campus |
+            -+------+-------------------------------+--------+-
+             | art  | School of Art & Design        | old    |
+             | bus  | School of Business            | south  |
+             | edu  | College of Education          | old    |
+             | eng  | School of Engineering         | north  |
+             | la   | School of Arts and Humanities | old    |
+             | mus  | School of Music & Dance       | south  |
+             | ns   | School of Natural Sciences    | old    |
+             | ph   | Public Honorariums            |        |
+             | sc   | School of Continuing Studies  |        |
+
+             ----
+             /school
+             SELECT "school"."code",
+                    "school"."name",
+                    "school"."campus"
+             FROM "school"
+             ORDER BY 1 ASC
+        - uri: /department[ee]{name, count(course)}/:pgsql_gw
+          status: 200 OK
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          - [Vary, Accept]
+          body: |2
+             | department                             |
+             +------------------------+---------------+
+             | name                   | count(course) |
+            -+------------------------+---------------+-
+             | Electrical Engineering |            14 |
+
+             ----
+             /department[ee]{name,count(course)}
+             SELECT "department"."name",
+                    COALESCE("course"."count", 0)
+             FROM "ad"."department"
+                  LEFT OUTER JOIN (SELECT COUNT(TRUE) AS "count",
+                                          "course"."department_code"
+                                   FROM "ad"."course"
+                                   GROUP BY 2) AS "course"
+                                  ON ("department"."code" = "course"."department_code")
+             WHERE ("department"."code" = 'ee')
+             ORDER BY "department"."code" ASC
+        - uri: /sqlite_gw(/school[art].program)/:json
+          status: 200 OK
+          headers:
+          - [Content-Type, application/javascript]
+          - [Content-Disposition, inline; filename="program.js"]
+          body: |
+            {
+              "program": [
+                {
+                  "school_code": "art",
+                  "code": "gart",
+                  "title": "Post Baccalaureate in Art History",
+                  "degree": "pb"
+                },
+                {
+                  "school_code": "art",
+                  "code": "uhist",
+                  "title": "Bachelor of Arts in Art History",
+                  "degree": "ba"
+                },
+                {
+                  "school_code": "art",
+                  "code": "ustudio",
+                  "title": "Bachelor of Arts in Studio Art",
+                  "degree": "ba"
+                }
+              ]
+            }
+        - uri: /pgsql_gw(/fetch(count(course)))/:raw
+          status: 200 OK
+          headers:
+          - [Content-Type, application/javascript]
+          - [Content-Disposition, inline; filename="count(course).js"]
+          body: |
+            {
+              "meta": {
+                "domain": {
+                  "type": "integer"
+                },
+                "header": "count(course)",
+                "syntax": "count(course)"
+              },
+              "data": 358
+            }
+        - uri: /pgsql_gw(/school/:html)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            invalid request: unsupported action
+        - uri: /sqlite_gw(/nothing)
+          status: 400 Bad Request
+          headers:
+          - [Content-Type, text/plain; charset=UTF-8]
+          body: |
+            bind error: unrecognized attribute 'nothing':
+                /sqlite_gw(/nothing)
+                            ^^^^^^^
       - id: tweak.meta
         tests:
         - ctl: [ext, tweak.meta]