opensysml 0.3.0__py3-none-any.whl

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.
opensysml/__init__.py ADDED
@@ -0,0 +1,355 @@
1
+ """opensysml - Python client library for OpenSysML SysML v2 parser."""
2
+
3
+ import warnings
4
+
5
+ from opensysml._version import VERSION as _declared_version
6
+ from opensysml.connection import Connection, DEFAULT_PORT, split_target
7
+ from opensysml.model import Model
8
+ from opensysml.symbol import Symbol
9
+ from opensysml.diagnostic import Diagnostic
10
+ from opensysml.enumeration import EnumLiteral
11
+ from opensysml.instance import Instance
12
+ from opensysml.typed import TypedObject
13
+ from opensysml.typefacts import (
14
+ AttributeFacts,
15
+ Multiplicity,
16
+ Specialization,
17
+ SymbolFacts,
18
+ TypeFacts,
19
+ )
20
+ from opensysml.capabilities import MissingCapabilityError, ServerInfo
21
+ from opensysml.values import UNSET, UnsetType
22
+ from opensysml.verdict import CalcResult, Verdict
23
+ from opensysml.query import QueryElement, QueryError
24
+ from opensysml.conversion import (
25
+ FORMAT_SYSML, FORMAT_TURTLE, Conversion, ExperimentalFeatureWarning,
26
+ format_of_path, is_experimental,
27
+ )
28
+ from opensysml.edit import AppliedEdit, EditResult, Editor
29
+ from opensysml.errors import (
30
+ OpenSysMLError, ChecksumMismatchError, ConnectionError, ConversionError,
31
+ EditError, EditResultError, EditTargetError, ExecutionError,
32
+ FeatureValueError, InvalidEditError, NoEditsError, OverlappingEditsError,
33
+ RenameReferencedError,
34
+ OwnerNotFoundError, OwnerNotNamespaceError, IllegalMemberKindError,
35
+ MemberNameTakenError, DeleteReferencedError,
36
+ InstanceTypeError, InvalidRequestError, ModelError,
37
+ ModelFileNotFoundError, ModelNotFoundError, ServiceError,
38
+ ServiceTimeoutError, StaleServiceError, SymbolNotFoundError,
39
+ TypeMismatchError, UnpinnedReleaseError, UnsupportedOperationError,
40
+ UnsupportedValueError, WrongKindError,
41
+ )
42
+
43
+ __all__ = [
44
+ "Connection", "Model", "Symbol", "Diagnostic", "EnumLiteral", "Instance",
45
+ "TypedObject", "TypeFacts", "Multiplicity", "Specialization", "SymbolFacts",
46
+ "AttributeFacts",
47
+ "ServerInfo",
48
+ "UNSET", "UnsetType",
49
+ "Conversion", "FORMAT_SYSML", "FORMAT_TURTLE", "format_of_path",
50
+ "ExperimentalFeatureWarning", "is_experimental",
51
+ "Editor", "EditResult", "AppliedEdit",
52
+ "Verdict", "CalcResult",
53
+ "QueryElement", "QueryError",
54
+ "OpenSysMLError", "ChecksumMismatchError", "ConnectionError",
55
+ "ConversionError", "ExecutionError", "FeatureValueError",
56
+ "EditError", "NoEditsError", "EditTargetError", "InvalidEditError",
57
+ "RenameReferencedError", "OverlappingEditsError", "EditResultError",
58
+ "OwnerNotFoundError", "OwnerNotNamespaceError", "IllegalMemberKindError",
59
+ "MemberNameTakenError", "DeleteReferencedError",
60
+ "InstanceTypeError", "InvalidRequestError", "MissingCapabilityError",
61
+ "ModelError", "ModelFileNotFoundError", "ModelNotFoundError",
62
+ "ServiceError", "ServiceTimeoutError", "StaleServiceError",
63
+ "SymbolNotFoundError",
64
+ "TypeMismatchError", "UnpinnedReleaseError",
65
+ "UnsupportedOperationError", "UnsupportedValueError",
66
+ "WrongKindError",
67
+ "load", "loads", "connect", "convert",
68
+ # "eval" is deprecated in favour of "evaluate", so it is not exported.
69
+ "evaluate", "instantiate",
70
+ "DEFAULT_PORT", "split_target",
71
+ "__version__"
72
+ ]
73
+
74
+ # The declaration ships beside this module, so this is the version of the code
75
+ # being imported: right for an editable install, whose dist-info metadata is
76
+ # frozen at install time, and the same value a wheel's metadata is built from.
77
+ __version__ = _declared_version
78
+
79
+ # Module-level default connection (lazy singleton)
80
+ _default_connection = None
81
+ _default_connection_params = None
82
+
83
+
84
+ def _get_default_connection(host='localhost', port=None):
85
+ """Get or create the default module-level connection.
86
+
87
+ If called with different host/port than last time, creates new connection.
88
+ A ``host:port`` address written as the host names the same service as the
89
+ two given separately, so it reuses the same connection.
90
+
91
+ Args:
92
+ host (str): Service hostname, or a ``host:port`` address
93
+ port (int, optional): Service port (default: 50051)
94
+
95
+ Returns:
96
+ Connection: Singleton default connection
97
+
98
+ Raises:
99
+ ValueError: If host names a port that is unreadable or disagrees with port
100
+ """
101
+ global _default_connection, _default_connection_params
102
+ host, port = split_target(host, port)
103
+ params = (host, port)
104
+
105
+ # Create new connection if params changed or no connection exists
106
+ if _default_connection is None or _default_connection_params != params:
107
+ # Close old connection to avoid refcount leak
108
+ if _default_connection is not None:
109
+ _default_connection.close()
110
+ _default_connection = Connection(host, port, auto_start=True)
111
+ _default_connection_params = params
112
+
113
+ return _default_connection
114
+
115
+
116
+ def loads(content, host='localhost', port=None, language=None, strict=False,
117
+ strict_conformance=False):
118
+ """Parse inline SysML or KerML content using the default connection."""
119
+ connection = (
120
+ _get_default_connection()
121
+ if host == 'localhost' and port is None
122
+ else _get_default_connection(host, port)
123
+ )
124
+ return connection.load_from_content(
125
+ content, strict=strict, language=language,
126
+ strict_conformance=strict_conformance
127
+ )
128
+
129
+
130
+ def load(file_path, host='localhost', port=None, strict=False,
131
+ strict_conformance=False):
132
+ """Load a SysML model from file using the default connection.
133
+
134
+ Convenience function that uses a module-level singleton connection.
135
+
136
+ Args:
137
+ file_path (str): Path to .sysml file
138
+ host (str): Service hostname, or a ``host:port`` address
139
+ port (int, optional): Service port (default: 50051)
140
+ strict (bool): Refuse a model the service reported errors for, rather
141
+ than returning one whose lookups fail later
142
+ strict_conformance (bool): Ask whether the file is conforming SysML v2:
143
+ notation only OpenSysML accepts is an error, not a warning
144
+
145
+ Returns:
146
+ Model: Parsed model object
147
+
148
+ Raises:
149
+ ModelFileNotFoundError: If the service cannot read file_path
150
+ ModelError: If strict and the model has error diagnostics
151
+ ConnectionError: If the service is unreachable
152
+ ValueError: If host names a port that is unreadable or disagrees with port
153
+ """
154
+ conn = _get_default_connection(host, port)
155
+ return conn.load(file_path, strict=strict,
156
+ strict_conformance=strict_conformance)
157
+
158
+
159
+ def connect(host='localhost', port=None, auto_start=True, version=None,
160
+ require_capabilities=None):
161
+ """Create a new connection to sysml-grpc service.
162
+
163
+ Convenience function that creates a new Connection instance.
164
+
165
+ Args:
166
+ host (str): Service hostname, or a ``host:port`` address, whose port is
167
+ used when no separate port is given (default: 'localhost')
168
+ port (int, optional): Service port (default: 50051)
169
+ auto_start (bool): If True, automatically start service if not running (default: True)
170
+ version (str, optional): Release tag the service must report, or
171
+ 'latest'; defaults to $OPENSYSML_GRPC_VERSION. Checked whether the
172
+ service is started here or managed by the caller
173
+ require_capabilities (iterable, optional): Capability names the service
174
+ must report, checked at connect time
175
+
176
+ Returns:
177
+ Connection: New connection instance
178
+
179
+ Raises:
180
+ ValueError: If host names a port that is unreadable or disagrees with port
181
+ StaleServiceError: If another release is already listening on the
182
+ address and this client may not stop it
183
+ MissingCapabilityError: If the service lacks a required capability
184
+
185
+ Example:
186
+ >>> conn = opensysml.connect("localhost:50123")
187
+ >>> conn.port
188
+ 50123
189
+ """
190
+ host, port = split_target(host, port)
191
+ return Connection(
192
+ host, port, auto_start=auto_start, version=version,
193
+ require_capabilities=require_capabilities,
194
+ )
195
+
196
+
197
+ def convert(to_format, file_path=None, content=None, model_hash=None,
198
+ from_format='', tolerate_syntax_errors=False, host='localhost',
199
+ port=None):
200
+ """Write a model out in another format (module-level convenience).
201
+
202
+ Args:
203
+ to_format (str): 'sysml', 'kerml', 'text', 'ttl', 'turtle' or 'rdf'
204
+ file_path (str, optional): Path the service reads the source from
205
+ content (str, optional): Source carried inline
206
+ model_hash (str, optional): Hash of a loaded model, whose parsed source
207
+ is converted
208
+ from_format (str, optional): Format to read the source as; inferred from
209
+ file_path's extension when omitted, notation for a model_hash, and
210
+ required for inline content
211
+ tolerate_syntax_errors (bool): Write notation back out even when the
212
+ parser could not read all of it
213
+ host (str): Service hostname, or a ``host:port`` address
214
+ port (int, optional): Service port (default: 50051)
215
+
216
+ Returns:
217
+ Conversion: The converted model; ``str()`` of it is the text
218
+
219
+ Warns:
220
+ ExperimentalFeatureWarning: If either format is RDF, whose mapping is
221
+ experimental — see ``docs/reference/rdf-mapping.md``
222
+
223
+ Example:
224
+ >>> import opensysml
225
+ >>> turtle = opensysml.convert("ttl", file_path="model.sysml")
226
+ >>> turtle.write("model.ttl")
227
+ 'model.ttl'
228
+ """
229
+ conn = _get_default_connection(host, port)
230
+ return conn.convert(
231
+ to_format,
232
+ file_path=file_path,
233
+ content=content,
234
+ model_hash=model_hash,
235
+ from_format=from_format,
236
+ tolerate_syntax_errors=tolerate_syntax_errors,
237
+ )
238
+
239
+
240
+ def evaluate(expression, file_path=None, model_hash=None, context_symbol_id=None,
241
+ host='localhost', port=None, subject=None):
242
+ """Evaluate a SysML expression (module-level convenience).
243
+
244
+ A model in hand has :meth:`Model.eval`, which needs neither the hash nor the
245
+ connection; this form is for an expression evaluated against a file.
246
+
247
+ Args:
248
+ expression (str): SysML expression
249
+ file_path (str, optional): Parse this file first, get model_hash
250
+ model_hash (str, optional): Use existing model hash
251
+ context_symbol_id (str, optional): Context for evaluation
252
+ host (str): Service hostname, or a ``host:port`` address
253
+ port (int, optional): Service port (default: 50051)
254
+ subject (str, optional): FQN of a part/usage to instantiate and evaluate
255
+ against, so a feature reads that object's value rather than the
256
+ declared default. Last, so a positional call written before it
257
+ still binds the address it meant
258
+
259
+ Returns:
260
+ Evaluated value
261
+
262
+ Raises:
263
+ ValueError: If neither file_path nor model_hash provided, if both are,
264
+ or if host names a port that is unreadable or disagrees with port
265
+ ExecutionError: If evaluation fails
266
+
267
+ Example:
268
+ >>> import opensysml
269
+ >>> result = opensysml.evaluate("2 + 2", file_path="test.sysml")
270
+ >>> print(result) # 4
271
+ """
272
+ conn = _get_default_connection(host, port)
273
+
274
+ # Validate params: exactly one of file_path or model_hash required
275
+ if file_path and model_hash:
276
+ raise ValueError("Provide either file_path or model_hash, not both")
277
+
278
+ if not file_path and not model_hash:
279
+ raise ValueError("Must provide either file_path or model_hash")
280
+
281
+ if file_path:
282
+ model = conn.load(file_path)
283
+ model_hash = model.hash
284
+
285
+ return conn.eval(
286
+ expression,
287
+ model_hash,
288
+ context_symbol_id=context_symbol_id,
289
+ subject_symbol_id=subject,
290
+ )
291
+
292
+
293
+ def instantiate(symbol_id, file_path=None, model_hash=None, host='localhost',
294
+ port=None):
295
+ """Instantiate a part/usage (module-level convenience).
296
+
297
+ A model in hand has :meth:`Model.instantiate`, which needs neither the hash
298
+ nor the connection; this form is for instantiating out of a file.
299
+
300
+ Args:
301
+ symbol_id (str): FQN of symbol to instantiate
302
+ file_path (str, optional): Parse this file first
303
+ model_hash (str, optional): Use existing model hash
304
+ host (str): Service hostname, or a ``host:port`` address
305
+ port (int, optional): Service port (default: 50051)
306
+
307
+ Returns:
308
+ Instance: Instance object
309
+
310
+ Raises:
311
+ ValueError: If neither file_path nor model_hash provided, if both are,
312
+ or if host names a port that is unreadable or disagrees with port
313
+ ExecutionError: If instantiation fails
314
+
315
+ Example:
316
+ >>> import opensysml
317
+ >>> instance = opensysml.instantiate("SPACECRAFT_WET", file_path="A1.sysml")
318
+ >>> print(instance.id)
319
+ """
320
+ conn = _get_default_connection(host, port)
321
+
322
+ # Validate params: exactly one of file_path or model_hash required
323
+ if file_path and model_hash:
324
+ raise ValueError("Provide either file_path or model_hash, not both")
325
+
326
+ if not file_path and not model_hash:
327
+ raise ValueError("Must provide either file_path or model_hash")
328
+
329
+ if file_path:
330
+ model = conn.load(file_path)
331
+ model_hash = model.hash
332
+
333
+ return conn.instantiate(symbol_id, model_hash)
334
+
335
+
336
+ #: Names that shadowed a built-in, and what each is called now. Served through
337
+ #: __getattr__ so a star-import no longer binds over the built-in.
338
+ _RENAMED_NAMES = {"eval": "evaluate", "RuntimeError": "ExecutionError"}
339
+
340
+
341
+ def __getattr__(name):
342
+ """Serve a renamed name with the object it became, warning about its use.
343
+
344
+ ``opensysml.eval`` is :func:`evaluate` and ``opensysml.RuntimeError`` is
345
+ :class:`~opensysml.errors.ExecutionError`, so existing snippets keep working.
346
+ """
347
+ replacement = _RENAMED_NAMES.get(name)
348
+ if replacement is None:
349
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
350
+ warnings.warn(
351
+ f"opensysml.{name} is deprecated; use opensysml.{replacement} instead",
352
+ DeprecationWarning,
353
+ stacklevel=2,
354
+ )
355
+ return globals()[replacement]
opensysml/_dist.py ADDED
@@ -0,0 +1,90 @@
1
+ """Where the installed opensysml distribution lives, and how it was installed.
2
+
3
+ An editable install keeps its dist-info in site-packages while its modules stay
4
+ in the checkout, so `Distribution.locate_file` answers with a site-packages path
5
+ that holds no `opensysml/` at all. Resolution therefore reads the PEP 610 record
6
+ the installer wrote, which names the directory the install was made from.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ from importlib.metadata import PackageNotFoundError, distribution
12
+ from urllib.parse import urlparse
13
+ from urllib.request import url2pathname
14
+
15
+ #: Layouts a project directory may hold its packages in.
16
+ _PACKAGE_ROOTS = ('', 'src')
17
+
18
+
19
+ def installed_distribution():
20
+ """The installed opensysml distribution.
21
+
22
+ Returns:
23
+ importlib.metadata.Distribution or None: None when the source tree is
24
+ not installed at all
25
+ """
26
+ try:
27
+ return distribution('opensysml')
28
+ except PackageNotFoundError:
29
+ return None
30
+
31
+
32
+ def editable_install(dist):
33
+ """Whether a distribution's modules are a checkout rather than copies of one.
34
+
35
+ Args:
36
+ dist (importlib.metadata.Distribution): The installed distribution
37
+
38
+ Returns:
39
+ bool: True for a PEP 660 editable install
40
+ """
41
+ return _direct_url_info(dist).get('dir_info', {}).get('editable') is True
42
+
43
+
44
+ def project_directory(dist):
45
+ """The directory an editable install was made from.
46
+
47
+ Args:
48
+ dist (importlib.metadata.Distribution): The installed distribution
49
+
50
+ Returns:
51
+ str or None: The directory, or None when the install records no local
52
+ one (a wheel from an index records no directory)
53
+ """
54
+ url = _direct_url_info(dist).get('url', '')
55
+ if not url.startswith('file:'):
56
+ return None
57
+ return os.path.realpath(url2pathname(urlparse(url).path))
58
+
59
+
60
+ def package_location(dist):
61
+ """Path of the ``opensysml/__init__.py`` a distribution installed.
62
+
63
+ Args:
64
+ dist (importlib.metadata.Distribution): The installed distribution
65
+
66
+ Returns:
67
+ str: The path, resolved through the install's own record for an editable
68
+ install and through the dist-info's directory otherwise. The path is
69
+ not guaranteed to exist: a distribution whose files are gone is
70
+ reported as it stands rather than guessed at.
71
+ """
72
+ project = project_directory(dist) if editable_install(dist) else None
73
+ if project is not None:
74
+ for root in _PACKAGE_ROOTS:
75
+ candidate = os.path.join(project, root, 'opensysml', '__init__.py')
76
+ if os.path.isfile(candidate):
77
+ return os.path.realpath(candidate)
78
+ return os.path.realpath(str(dist.locate_file('opensysml/__init__.py')))
79
+
80
+
81
+ def _direct_url_info(dist):
82
+ """The PEP 610 record of how a distribution was installed, as a dict."""
83
+ recorded = dist.read_text('direct_url.json')
84
+ if not recorded:
85
+ return {}
86
+ try:
87
+ info = json.loads(recorded)
88
+ except json.JSONDecodeError:
89
+ return {}
90
+ return info if isinstance(info, dict) else {}
opensysml/_version.py ADDED
@@ -0,0 +1,9 @@
1
+ """The one place the opensysml version is written.
2
+
3
+ `pyproject.toml` reads `VERSION` from here as the distribution's version, and
4
+ `opensysml.__version__` reports the version of the *installed* distribution, so
5
+ neither is a second copy that can drift. `scripts/check_version.py` compares
6
+ this value with the release tag before anything is uploaded.
7
+ """
8
+
9
+ VERSION = "0.3.0"