oculix-operix 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oculix-org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: oculix-operix
3
+ Version: 1.1.0
4
+ Summary: Visual automation for the real world — Python wrapper for OculiX
5
+ Author-email: Julien Mer <julien.mer38@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ajayrakde/Operix
8
+ Project-URL: Repository, https://github.com/ajayrakde/Operix
9
+ Keywords: visual-testing,automation,ocr,sikuli,gui-testing
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Software Development :: Testing
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # oculix (Python)
20
+
21
+ Python wrapper for [OculiX](https://github.com/oculix-org/Oculix) — visual automation for the real world.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install oculix-operix
27
+ ```
28
+
29
+ Requirements: Python 3.8+, Java 11+ on `PATH` (Eclipse Temurin or Azul Zulu).
30
+
31
+ The OculiX engine (~200 MB fat JAR) is downloaded on first use into `~/.oculix/lib/`.
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from oculix import Screen, App, VNCScreen
37
+
38
+ screen = Screen()
39
+ screen.click("login.png")
40
+ screen.type("admin")
41
+ screen.click("submit.png")
42
+ screen.wait("dashboard.png", 10)
43
+
44
+ # Open a desktop app
45
+ calc = App.open("calc")
46
+ screen.click("button_7.png")
47
+
48
+ # Drive a remote VNC display
49
+ vnc = VNCScreen.start("10.0.0.42", 5900, "", 1920, 1080)
50
+ vnc.click("logo.png")
51
+ vnc.stop()
52
+ ```
53
+
54
+ ## With pytest
55
+
56
+ ```python
57
+ import pytest
58
+ from oculix import Screen, App
59
+
60
+ @pytest.fixture
61
+ def calc():
62
+ a = App.open("calculator")
63
+ yield a
64
+ a.close()
65
+
66
+ def test_addition(calc):
67
+ s = Screen()
68
+ s.click("button_7.png")
69
+ s.click("button_plus.png")
70
+ s.click("button_3.png")
71
+ s.click("button_equals.png")
72
+ assert s.exists("result_10.png")
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT
78
+
79
+ ## Java API parity
80
+
81
+ This fork exposes the complete pinned Oculix 4.0.0 public method surface: 211
82
+ public types with named Python methods, constructor/field access and every Java
83
+ overload's arguments. `from oculix import Screen, OCR, Pattern` stays unchanged.
84
+ Every class is also available through `oculix.java`; nested classes keep names
85
+ such as `OCR.Options` and `OCR.PSM`.
86
+
87
+ ```python
88
+ from oculix import Screen, Pattern, Match, OCR, Location, JavaCallback, ObserverCallBack
89
+
90
+ screen = Screen()
91
+ match = screen.find(Pattern("button.png").similar(0.8))
92
+ assert isinstance(match, Match)
93
+ match.click()
94
+ screen.type("hello", 0)
95
+
96
+ options = OCR.Options().language("eng").psm(OCR.PSM.SINGLE_LINE)
97
+ text = OCR.readText("button.png", options)
98
+
99
+ # Ambiguous Java overloads can be selected explicitly.
100
+ options.psm.overload("org.sikuli.script.OCR$PSM")("SINGLE_LINE")
101
+ ```
102
+
103
+ `exists()` returns `Match` or `None`, matching Java; truth checks continue to work.
104
+ Java interface callbacks accept `JavaCallback(interface_name, handler)` and
105
+ observer methods accept a Python function directly. Arrays/collections become
106
+ Python sequences; maps become dictionaries; Java iterators support iteration.
107
+
108
+ This distribution uses the matching 1.1.0 JVM bridge from `ajayrakde/Operix`.
109
+ Do not mix it with the older upstream bridge. It occupies the same `oculix` import
110
+ namespace as upstream, so install only one of the two distributions in a virtual
111
+ environment. See [implementation and validation](https://github.com/ajayrakde/Operix/blob/python/java-api-parity/docs/python-api-parity.md).
@@ -0,0 +1,93 @@
1
+ # oculix (Python)
2
+
3
+ Python wrapper for [OculiX](https://github.com/oculix-org/Oculix) — visual automation for the real world.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install oculix-operix
9
+ ```
10
+
11
+ Requirements: Python 3.8+, Java 11+ on `PATH` (Eclipse Temurin or Azul Zulu).
12
+
13
+ The OculiX engine (~200 MB fat JAR) is downloaded on first use into `~/.oculix/lib/`.
14
+
15
+ ## Quickstart
16
+
17
+ ```python
18
+ from oculix import Screen, App, VNCScreen
19
+
20
+ screen = Screen()
21
+ screen.click("login.png")
22
+ screen.type("admin")
23
+ screen.click("submit.png")
24
+ screen.wait("dashboard.png", 10)
25
+
26
+ # Open a desktop app
27
+ calc = App.open("calc")
28
+ screen.click("button_7.png")
29
+
30
+ # Drive a remote VNC display
31
+ vnc = VNCScreen.start("10.0.0.42", 5900, "", 1920, 1080)
32
+ vnc.click("logo.png")
33
+ vnc.stop()
34
+ ```
35
+
36
+ ## With pytest
37
+
38
+ ```python
39
+ import pytest
40
+ from oculix import Screen, App
41
+
42
+ @pytest.fixture
43
+ def calc():
44
+ a = App.open("calculator")
45
+ yield a
46
+ a.close()
47
+
48
+ def test_addition(calc):
49
+ s = Screen()
50
+ s.click("button_7.png")
51
+ s.click("button_plus.png")
52
+ s.click("button_3.png")
53
+ s.click("button_equals.png")
54
+ assert s.exists("result_10.png")
55
+ ```
56
+
57
+ ## License
58
+
59
+ MIT
60
+
61
+ ## Java API parity
62
+
63
+ This fork exposes the complete pinned Oculix 4.0.0 public method surface: 211
64
+ public types with named Python methods, constructor/field access and every Java
65
+ overload's arguments. `from oculix import Screen, OCR, Pattern` stays unchanged.
66
+ Every class is also available through `oculix.java`; nested classes keep names
67
+ such as `OCR.Options` and `OCR.PSM`.
68
+
69
+ ```python
70
+ from oculix import Screen, Pattern, Match, OCR, Location, JavaCallback, ObserverCallBack
71
+
72
+ screen = Screen()
73
+ match = screen.find(Pattern("button.png").similar(0.8))
74
+ assert isinstance(match, Match)
75
+ match.click()
76
+ screen.type("hello", 0)
77
+
78
+ options = OCR.Options().language("eng").psm(OCR.PSM.SINGLE_LINE)
79
+ text = OCR.readText("button.png", options)
80
+
81
+ # Ambiguous Java overloads can be selected explicitly.
82
+ options.psm.overload("org.sikuli.script.OCR$PSM")("SINGLE_LINE")
83
+ ```
84
+
85
+ `exists()` returns `Match` or `None`, matching Java; truth checks continue to work.
86
+ Java interface callbacks accept `JavaCallback(interface_name, handler)` and
87
+ observer methods accept a Python function directly. Arrays/collections become
88
+ Python sequences; maps become dictionaries; Java iterators support iteration.
89
+
90
+ This distribution uses the matching 1.1.0 JVM bridge from `ajayrakde/Operix`.
91
+ Do not mix it with the older upstream bridge. It occupies the same `oculix` import
92
+ namespace as upstream, so install only one of the two distributions in a virtual
93
+ environment. See [implementation and validation](https://github.com/ajayrakde/Operix/blob/python/java-api-parity/docs/python-api-parity.md).
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oculix-operix"
7
+ version = "1.1.0"
8
+ description = "Visual automation for the real world — Python wrapper for OculiX"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.8"
12
+ authors = [{name = "Julien Mer", email = "julien.mer38@gmail.com"}]
13
+ keywords = ["visual-testing", "automation", "ocr", "sikuli", "gui-testing"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Topic :: Software Development :: Testing",
19
+ ]
20
+ dependencies = []
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/ajayrakde/Operix"
24
+ Repository = "https://github.com/ajayrakde/Operix"
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["src"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
31
+
32
+ [tool.setuptools.package-data]
33
+ oculix = ["_api_schema.json.gz", "py.typed", "**/*.pyi"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """Complete named Python facade for the pinned Oculix Java API.
2
+
3
+ Java method names, constructors and overload arguments are preserved.
4
+ Use ``method.overload(*java_type_names)`` for ambiguous null/reference calls.
5
+ """
6
+ from ._bridge import Bridge, BridgeError, RemoteObject, RemoteWrapper, default_bridge
7
+ from ._api import JavaObject, JavaCallback, java_class
8
+ from .java import *
9
+ from .java import __all__ as _java_names
10
+
11
+ __version__ = '1.1.0'
12
+ __all__ = list(_java_names) + ['Bridge', 'BridgeError', 'JavaObject', 'JavaCallback', 'java_class', 'default_bridge']
@@ -0,0 +1,4 @@
1
+ from .java import *
2
+ from ._bridge import Bridge, BridgeError, default_bridge
3
+ from ._api import JavaObject, JavaCallback, java_class
4
+ __version__: str
@@ -0,0 +1,269 @@
1
+ """Java overload binding, fields and typed facades for the generated API."""
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import gzip
6
+ import json
7
+ import keyword
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ._bridge import RemoteWrapper, RemoteObject, register_wrapper, _WRAPPER_TYPES
12
+
13
+ _SCHEMA = json.loads(gzip.decompress(Path(__file__).with_name('_api_schema.json.gz').read_bytes()))
14
+ _TYPES = {}
15
+
16
+
17
+ def _default_bridge():
18
+ import oculix
19
+ return oculix.default_bridge()
20
+
21
+
22
+ def _identifier(name):
23
+ return name + '_' if keyword.iskeyword(name) or name in {'self', 'cls'} else name
24
+
25
+
26
+ def _is_java_type(actual, formal):
27
+ if actual == formal or formal == 'java.lang.Object': return True
28
+ seen = set()
29
+ pending = [actual]
30
+ while pending:
31
+ name = pending.pop()
32
+ if name == formal: return True
33
+ if name in seen: continue
34
+ seen.add(name)
35
+ row = _SCHEMA.get(name, {})
36
+ pending.extend(row.get('interfaces', []))
37
+ if row.get('superclass'): pending.append(row['superclass'])
38
+ return False
39
+
40
+
41
+ def _score(value, formal):
42
+ if value is None:
43
+ return -1 if formal in {'boolean','byte','short','int','long','float','double','char'} else 0
44
+ if isinstance(value, RemoteWrapper): value = value._remote
45
+ if isinstance(value, RemoteObject):
46
+ if value._class == formal: return 12
47
+ if _is_java_type(value._class, formal): return 8 if formal != 'java.lang.Object' else 1
48
+ # External Java inheritance is verified by the JVM resolver.
49
+ return 2 if value._class not in _SCHEMA else -1
50
+ if isinstance(value, JavaCallback): return 8 if formal in {value.interface, 'java.lang.Object'} else -1
51
+ if callable(value): return 5 if formal == 'java.lang.Object' or _SCHEMA.get(formal, {}).get('interface') else -1
52
+ if formal == 'java.lang.Object': return 1
53
+ if type(value) is bool: return 12 if formal in {'boolean','java.lang.Boolean'} else -1
54
+ if type(value) is int:
55
+ if formal in {'int','java.lang.Integer'}: return 12 if -(2**31) <= value < 2**31 else -1
56
+ if formal in {'long','java.lang.Long'}: return 10 if -(2**63) <= value < 2**63 else -1
57
+ if formal in {'short','java.lang.Short'}: return 8 if -(2**15) <= value < 2**15 else -1
58
+ if formal in {'byte','java.lang.Byte'}: return 7 if -128 <= value <= 127 else -1
59
+ return 5 if formal in {'double','float','java.lang.Double','java.lang.Float'} else -1
60
+ if isinstance(value, float):
61
+ return 12 if formal in {'double','java.lang.Double'} else 8 if formal in {'float','java.lang.Float'} else -1
62
+ if isinstance(value, str):
63
+ if formal == 'java.lang.String': return 12
64
+ if formal == 'java.lang.CharSequence': return 10
65
+ if formal in {'char','java.lang.Character'}: return 8 if len(value) == 1 else -1
66
+ return 5 if _SCHEMA.get(formal, {}).get('enum') else -1
67
+ if isinstance(value, (bytes, bytearray)) and formal == 'byte[]': return 12
68
+ if isinstance(value, (list, tuple, bytes, bytearray)):
69
+ if formal.endswith('[]'):
70
+ scores = [_score(v, formal[:-2]) for v in value]
71
+ return -1 if any(v < 0 for v in scores) else 8
72
+ return 6 if formal in {'java.util.List','java.util.Collection','java.lang.Iterable','java.util.Set'} else -1
73
+ if isinstance(value, dict): return 8 if formal == 'java.util.Map' else -1
74
+ return -1
75
+
76
+
77
+ def _bind(member, args, kwargs):
78
+ parameters = member['parameters']
79
+ values = list(args)
80
+ varargs = member['varargs']
81
+ fixed = len(parameters) - int(varargs)
82
+ if not varargs and len(values) > len(parameters): raise TypeError('Too many positional arguments')
83
+ for i, p in enumerate(parameters):
84
+ name = _identifier(p['name'])
85
+ if name in kwargs:
86
+ if i < len(args): raise TypeError('Multiple values for ' + name)
87
+ if i != len(values): raise TypeError('Missing preceding argument')
88
+ values.append(kwargs[name])
89
+ elif i >= len(values) and i < fixed:
90
+ raise TypeError('Missing argument ' + name)
91
+ known = {_identifier(p['name']) for p in parameters}
92
+ if set(kwargs) - known: raise TypeError('Unknown argument: ' + ', '.join(set(kwargs) - known))
93
+ if varargs:
94
+ # Both Java's explicit array form and Python's expanded positional form.
95
+ if len(values) == len(parameters) and isinstance(values[-1], (list, tuple)):
96
+ pass
97
+ else: values = values[:fixed] + [values[fixed:]]
98
+ elif len(values) != len(parameters): raise TypeError('Wrong argument count')
99
+ scores = [_score(v, p['type']) for v, p in zip(values, parameters)]
100
+ if any(s < 0 for s in scores): raise TypeError('Argument types do not match')
101
+ return values, sum(scores) - int(varargs)
102
+
103
+
104
+ class BoundJavaMethod:
105
+ def __init__(self, descriptor, instance, owner):
106
+ self.descriptor, self.instance, self.owner = descriptor, instance, owner
107
+ self.__name__ = descriptor.name
108
+ self.__doc__ = descriptor.__doc__
109
+ self.overloads = tuple(descriptor.members)
110
+ member = next((m for m in descriptor.members if m['static'] or instance is not None), descriptor.members[0])
111
+ params = [inspect.Parameter(_identifier(p['name']), inspect.Parameter.VAR_POSITIONAL
112
+ if member['varargs'] and i == len(member['parameters']) - 1 else inspect.Parameter.POSITIONAL_OR_KEYWORD)
113
+ for i, p in enumerate(member['parameters'])]
114
+ self.__signature__ = inspect.Signature(params)
115
+
116
+ def overload(self, *parameter_types):
117
+ """Choose an exact Java overload where Python values cannot disambiguate it."""
118
+ members = [m for m in self.descriptor.members if tuple(p['type'] for p in m['parameters']) == parameter_types]
119
+ if not members: raise TypeError('No Java overload with types ' + repr(parameter_types))
120
+ return BoundJavaMethod(JavaMethod(self.descriptor.name, members), self.instance, self.owner)
121
+
122
+ def __call__(self, *args, **kwargs):
123
+ candidates = []
124
+ for m in self.descriptor.members:
125
+ if self.instance is None and not m['static'] and self.__name__ != '<init>': continue
126
+ try: values, score = _bind(m, args, kwargs)
127
+ except TypeError: continue
128
+ candidates.append((score, m, values))
129
+ if not candidates:
130
+ raise TypeError(f'No matching overload for {self.owner.JAVA_CLASS}.{self.__name__}; see .overloads')
131
+ best_score = max(c[0] for c in candidates)
132
+ best = [c for c in candidates if c[0] == best_score]
133
+ # Drop less specific reference overloads for null/reference arguments.
134
+ if len(best) > 1:
135
+ specific = [c for c in best if all(c is other or all(
136
+ a['type'] == b['type'] or _is_java_type(a['type'], b['type'])
137
+ for a, b in zip(c[1]['parameters'], other[1]['parameters'])) for other in best)]
138
+ if len(specific) == 1: best = specific
139
+ signatures = {tuple(p['type'] for p in c[1]['parameters']) for c in best}
140
+ if len(signatures) > 1:
141
+ raise TypeError(f'Ambiguous Java overload for {self.__name__}: {sorted(signatures)}; use .overload(*types)')
142
+ _, member, values = best[0]
143
+ bridge = self.instance._remote._bridge if self.instance is not None else _default_bridge()
144
+ prepared = []
145
+ for p, v in zip(member['parameters'], values):
146
+ if callable(v) and not isinstance(v, JavaCallback):
147
+ interface = p['type']
148
+ if interface == 'java.lang.Object': interface = 'org.sikuli.script.ObserverCallBack'
149
+ v = JavaCallback(interface, v)
150
+ prepared.append(v)
151
+ types = [p['type'] for p in member['parameters']]
152
+ if self.__name__ == '<init>': return bridge.create(self.owner.JAVA_CLASS, prepared, parameter_types=types)
153
+ if member['static']:
154
+ return bridge.call_static(self.owner.JAVA_CLASS, self.__name__, prepared, parameter_types=types)
155
+ return bridge.call(self.instance._remote._ref, self.__name__, prepared, parameter_types=types)
156
+
157
+
158
+ class JavaMethod:
159
+ """A declared, finite set of Java overloads; no arbitrary method forwarding."""
160
+ def __init__(self, name, members):
161
+ self.name, self.members = name, members
162
+ self.__doc__ = '\n'.join(m['name'] + '(' + ', '.join(p['type'] + ' ' + p['name'] for p in m['parameters']) + ')' for m in members)
163
+
164
+ def __get__(self, instance, owner=None):
165
+ return BoundJavaMethod(self, instance, owner)
166
+
167
+
168
+ class JavaField:
169
+ def __init__(self, field): self.field = field
170
+ def __get__(self, instance, owner=None):
171
+ if instance is None and not self.field['static']: return self
172
+ bridge = instance._remote._bridge if instance is not None else _default_bridge()
173
+ req = {'class': owner.JAVA_CLASS, 'field': self.field['name']}
174
+ if instance is not None and not self.field['static']: req = {'ref': instance._remote._ref, 'field': self.field['name']}
175
+ from ._bridge import _decode
176
+ return _decode(bridge, bridge._request(req))
177
+ def __set__(self, instance, value):
178
+ self.set(type(instance), instance, value)
179
+ def set(self, owner, instance, value):
180
+ if self.field['final']: raise AttributeError('Java field is final: ' + self.field['name'])
181
+ if _score(value, self.field['type']) < 0: raise TypeError('Value does not match Java field type ' + self.field['type'])
182
+ bridge = instance._remote._bridge if instance is not None else _default_bridge()
183
+ from ._bridge import _encode
184
+ req = {'class': owner.JAVA_CLASS, 'field': self.field['name'], 'value': _encode(value, bridge)}
185
+ if instance is not None and not self.field['static']: req['ref'] = instance._remote._ref
186
+ bridge._request(req)
187
+
188
+
189
+ class JavaMeta(type):
190
+ def __setattr__(cls, name, value):
191
+ descriptor = next((p.__dict__[name] for p in cls.__mro__ if name in p.__dict__), None)
192
+ if isinstance(descriptor, JavaField): descriptor.set(cls, None, value)
193
+ else: super().__setattr__(name, value)
194
+
195
+
196
+ class JavaObject(RemoteWrapper, metaclass=JavaMeta):
197
+ JAVA_CLASS = 'java.lang.Object'
198
+ def __init__(self, *args, **kwargs):
199
+ schema = _SCHEMA.get(self.JAVA_CLASS)
200
+ if schema is None: result = _default_bridge().create(self.JAVA_CLASS, list(args))
201
+ else:
202
+ if not schema['constructors']: raise TypeError(self.JAVA_CLASS + ' has no public constructors')
203
+ result = BoundJavaMethod(JavaMethod('<init>', schema['constructors']), None, type(self))(*args, **kwargs)
204
+ self._remote = result._remote if isinstance(result, RemoteWrapper) else result
205
+ self._remote._bridge._cache[self._remote._ref] = self
206
+ @property
207
+ def _ref(self): return self._remote._ref
208
+ @property
209
+ def _bridge(self): return self._remote._bridge
210
+ def __getattr__(self, name):
211
+ if self.JAVA_CLASS in _SCHEMA: raise AttributeError(name)
212
+ return super().__getattr__(name)
213
+ def __iter__(self):
214
+ if self._remote._class.startswith('java.util.'):
215
+ iterator = self if self._remote._kind == 'iterator' or 'Iterator' in self._remote._class else self._call('iterator')
216
+ while iterator._call('hasNext'): yield iterator._call('next')
217
+ else: raise TypeError(self.JAVA_CLASS + ' is not an iterable')
218
+ def __repr__(self): return f'<{type(self).__name__} {self._remote._class}#{self._remote._ref}>'
219
+
220
+
221
+ class JavaCallback:
222
+ """Python implementation of a Java interface or ObserverCallBack.
223
+
224
+ handler may be a function (single method) or an object/dict of method handlers.
225
+ Keep this object alive while Java may invoke it; the bridge retains it until stop.
226
+ """
227
+ def __init__(self, interface, handler):
228
+ self.interface = interface.JAVA_CLASS if hasattr(interface, 'JAVA_CLASS') else interface
229
+ self.handler = handler
230
+ def dispatch(self, method, args):
231
+ if isinstance(self.handler, dict): function = self.handler[method]
232
+ elif callable(self.handler): function = self.handler
233
+ else: function = getattr(self.handler, method)
234
+ return function(*args)
235
+
236
+
237
+ def java_class(name):
238
+ if name in _TYPES: return _TYPES[name]
239
+ cls = JavaMeta(name.rsplit('.', 1)[-1].replace('$', '_'), (JavaObject,), {'JAVA_CLASS': name, '__module__': 'oculix.java'})
240
+ _TYPES[name] = cls
241
+ register_wrapper(name, cls)
242
+ return cls
243
+
244
+
245
+ def load_api(namespace):
246
+ todo = set(_SCHEMA)
247
+ while todo:
248
+ ready = sorted(n for n in todo if _SCHEMA[n]['superclass'] not in todo)
249
+ for name in ready:
250
+ row = _SCHEMA[name]
251
+ base = _TYPES.get(row['superclass'], JavaObject)
252
+ attrs = {'JAVA_CLASS': name, '__module__': 'oculix.java', '__doc__': 'Java ' + name}
253
+ groups = {}
254
+ for m in row['methods']: groups.setdefault(m['name'], []).append(m)
255
+ for mname, members in groups.items():
256
+ descriptor = JavaMethod(mname, members)
257
+ attrs[_identifier(mname)] = descriptor
258
+ attrs[mname] = descriptor
259
+ for f in row['fields']:
260
+ if _identifier(f['name']) not in attrs: attrs[_identifier(f['name'])] = JavaField(f)
261
+ cls = JavaMeta(row['python_name'], (base,), attrs)
262
+ _TYPES[name] = cls
263
+ register_wrapper(name, cls)
264
+ namespace[row['python_name']] = cls
265
+ todo -= set(ready)
266
+ for name, cls in _TYPES.items():
267
+ if '$' in name and name.rsplit('$', 1)[0] in _TYPES:
268
+ setattr(_TYPES[name.rsplit('$', 1)[0]], name.rsplit('$', 1)[1], cls)
269
+ namespace['__all__'] = sorted(row['python_name'] for row in _SCHEMA.values())