ggblab 0.9.3__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.
- ggblab/__init__.py +44 -0
- ggblab/_version.py +4 -0
- ggblab/comm.py +243 -0
- ggblab/construction.py +179 -0
- ggblab/errors.py +142 -0
- ggblab/ggbapplet.py +293 -0
- ggblab/parser.py +486 -0
- ggblab/persistent_counter.py +175 -0
- ggblab/schema.py +114 -0
- ggblab/utils.py +109 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/build_log.json +730 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/install.json +5 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/package.json +210 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/package.json.orig +205 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/schemas/ggblab/plugin.json +8 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js +465 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/lib_index_js.bbfa36bc62ee08eb62b2.js.map +1 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js +568 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/remoteEntry.2d29364aef8b527d773e.js.map +1 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style.js +4 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js +492 -0
- ggblab-0.9.3.data/data/share/jupyter/labextensions/ggblab/static/style_index_js.aab9f5416f41ce79cac3.js.map +1 -0
- ggblab-0.9.3.dist-info/METADATA +768 -0
- ggblab-0.9.3.dist-info/RECORD +26 -0
- ggblab-0.9.3.dist-info/WHEEL +4 -0
- ggblab-0.9.3.dist-info/licenses/LICENSE +29 -0
ggblab/schema.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
import xmlschema
|
|
4
|
+
import io
|
|
5
|
+
# from pprint import pprint
|
|
6
|
+
|
|
7
|
+
class ggb_schema:
|
|
8
|
+
"""GeoGebra XML schema loader and validator.
|
|
9
|
+
|
|
10
|
+
Manages the GeoGebra XML schema (XSD) for validating and parsing .ggb
|
|
11
|
+
construction files. The schema is automatically downloaded from the
|
|
12
|
+
official GeoGebra site and cached locally for offline use.
|
|
13
|
+
|
|
14
|
+
The schema enables:
|
|
15
|
+
- XML validation of GeoGebra constructions
|
|
16
|
+
- Conversion between XML and Python dictionaries
|
|
17
|
+
- Type-safe parsing of construction elements
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
url (str): URL of the GeoGebra common.xsd schema file
|
|
21
|
+
local_path (str): Local cache path for the downloaded schema
|
|
22
|
+
schema_content (str): Raw XSD content as string
|
|
23
|
+
schema (xmlschema.XMLSchema): Compiled schema object for validation
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> schema = ggb_schema()
|
|
27
|
+
>>> # Schema is loaded and ready for use
|
|
28
|
+
>>> data_dict = schema.schema.to_dict(xml_string)
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
The schema is downloaded once and cached in xsd/common.xsd.
|
|
32
|
+
Delete the cache to force re-download on next instantiation.
|
|
33
|
+
"""
|
|
34
|
+
url = 'http://www.geogebra.org/apps/xsd/common.xsd'
|
|
35
|
+
local_path = 'xsd/common.xsd'
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
"""Initialize the schema loader and load/cache the XSD file.
|
|
39
|
+
|
|
40
|
+
Downloads the GeoGebra schema from the official URL if not already
|
|
41
|
+
cached locally. Creates the cache directory if it doesn't exist.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
xmlschema.validators.exceptions.XMLSchemaValidationError: If schema is invalid.
|
|
45
|
+
Exception: If schema download or loading fails.
|
|
46
|
+
"""
|
|
47
|
+
# Ensure the cache directory exists
|
|
48
|
+
os.makedirs(os.path.dirname(self.local_path), exist_ok=True)
|
|
49
|
+
self.schema_content = cache_schema_locally(self.url, self.local_path)
|
|
50
|
+
|
|
51
|
+
# Assuming you have a geogebra.xml file (from an unzipped .ggb file)
|
|
52
|
+
# and the XSD files (ggb.xsd and common.xsd) downloaded locally
|
|
53
|
+
# or you can use the URL for the schema
|
|
54
|
+
|
|
55
|
+
# Create a schema instance (it automatically handles imported common.xsd)
|
|
56
|
+
try:
|
|
57
|
+
self.schema = xmlschema.XMLSchema(io.StringIO(self.schema_content))
|
|
58
|
+
|
|
59
|
+
# Convert the XML data to a Python dictionary
|
|
60
|
+
# data_dict = ggb_schema.to_dict(io.StringIO(r))
|
|
61
|
+
|
|
62
|
+
# Pretty print the resulting dictionary
|
|
63
|
+
# pprint(data_dict)
|
|
64
|
+
|
|
65
|
+
except xmlschema.validators.exceptions.XMLSchemaValidationError as e:
|
|
66
|
+
print(f"XML validation error: {e}")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"An error occurred: {e}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cache_schema_locally(schema_url, local_file_path):
|
|
72
|
+
"""Download and cache a schema file from URL.
|
|
73
|
+
|
|
74
|
+
Downloads an XML schema from the specified URL and saves it to a local
|
|
75
|
+
file for offline use. If the file already exists, uses the cached version
|
|
76
|
+
instead of re-downloading.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
schema_url (str): URL of the schema file to download.
|
|
80
|
+
local_file_path (str): Path where the schema should be cached.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: Content of the schema file, or None if download fails.
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
>>> content = cache_schema_locally(
|
|
87
|
+
... 'http://example.com/schema.xsd',
|
|
88
|
+
... 'cache/schema.xsd'
|
|
89
|
+
... )
|
|
90
|
+
Using local cached file: cache/schema.xsd
|
|
91
|
+
|
|
92
|
+
Note:
|
|
93
|
+
Future enhancement: Add logic to check file age or Last-Modified
|
|
94
|
+
header to refresh stale cached schemas.
|
|
95
|
+
"""
|
|
96
|
+
if os.path.exists(local_file_path):
|
|
97
|
+
print(f"Using local cached file: {local_file_path}")
|
|
98
|
+
with open(local_file_path, 'r', encoding='utf-8') as f:
|
|
99
|
+
return f.read()
|
|
100
|
+
|
|
101
|
+
print(f"Local file not found. Downloading from: {schema_url}")
|
|
102
|
+
try:
|
|
103
|
+
response = requests.get(schema_url)
|
|
104
|
+
response.raise_for_status() # Raise an exception for bad status codes
|
|
105
|
+
|
|
106
|
+
# Save the content to the local file
|
|
107
|
+
with open(local_file_path, 'w', encoding='utf-8') as f:
|
|
108
|
+
f.write(response.text)
|
|
109
|
+
print(f"Successfully downloaded and saved to: {local_file_path}")
|
|
110
|
+
return response.text
|
|
111
|
+
|
|
112
|
+
except requests.exceptions.RequestException as e:
|
|
113
|
+
print(f"Error downloading schema: {e}")
|
|
114
|
+
return None
|
ggblab/utils.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Common utility functions for ggblab.
|
|
2
|
+
|
|
3
|
+
Python's Design Grievances
|
|
4
|
+
==========================
|
|
5
|
+
|
|
6
|
+
This module exists because Python's standard library refuses to include basic
|
|
7
|
+
utilities that every developer needs. Here are some legitimate complaints:
|
|
8
|
+
|
|
9
|
+
1. **flatten() is not standardized**
|
|
10
|
+
|
|
11
|
+
Despite being one of the most common operations, Python forces you to either:
|
|
12
|
+
- Use itertools.chain.from_iterable() (only 1-level deep)
|
|
13
|
+
- Install more-itertools (external dependency)
|
|
14
|
+
- Write it yourself every single time
|
|
15
|
+
|
|
16
|
+
JavaScript has Array.flat(). Why doesn't Python have list.flatten()?
|
|
17
|
+
|
|
18
|
+
The excuse: "Strings are iterable, so it's ambiguous."
|
|
19
|
+
The reality: This is a solvable problem. Just treat str/bytes as atomic by default.
|
|
20
|
+
|
|
21
|
+
2. **String as Iterable: The Perpetual Footgun**
|
|
22
|
+
|
|
23
|
+
str being iterable causes endless bugs:
|
|
24
|
+
|
|
25
|
+
>>> def process(items):
|
|
26
|
+
... return [x for item in items for x in item]
|
|
27
|
+
>>> process(['abc', 'def']) # Expected: ['abc', 'def']
|
|
28
|
+
['a', 'b', 'c', 'd', 'e', 'f'] # Oops!
|
|
29
|
+
|
|
30
|
+
We have to write `isinstance(x, (str, bytes))` checks EVERYWHERE.
|
|
31
|
+
This is a design flaw, not a feature.
|
|
32
|
+
|
|
33
|
+
3. **Pattern Matching Underutilized (Since Python 3.10)**
|
|
34
|
+
|
|
35
|
+
Python 3.10 introduced structural pattern matching with beautiful tuple unpacking:
|
|
36
|
+
|
|
37
|
+
>>> match edges:
|
|
38
|
+
... case []:
|
|
39
|
+
... return "no edges"
|
|
40
|
+
... case [single]:
|
|
41
|
+
... return f"one edge: {single}"
|
|
42
|
+
... case [first, second]:
|
|
43
|
+
... return f"two edges: {first}, {second}"
|
|
44
|
+
... case [first, *rest]:
|
|
45
|
+
... return f"multiple edges starting with {first}"
|
|
46
|
+
|
|
47
|
+
Yet most Python code still uses:
|
|
48
|
+
- if len(edges) == 0: ...
|
|
49
|
+
- if len(edges) == 1: x = edges[0]; ...
|
|
50
|
+
- if len(edges) >= 2: first, second = edges[0], edges[1]; ...
|
|
51
|
+
|
|
52
|
+
Why? Because Python educators haven't caught up with modern features.
|
|
53
|
+
The language evolves, but teaching materials stay stuck in 2015.
|
|
54
|
+
|
|
55
|
+
4. **Education Gap: Modern Python Features Ignored**
|
|
56
|
+
|
|
57
|
+
Python keeps adding excellent features that go unused:
|
|
58
|
+
- Walrus operator (:=) for cleaner loops
|
|
59
|
+
- Union types (X | Y instead of Union[X, Y])
|
|
60
|
+
- Structural pattern matching (match/case)
|
|
61
|
+
- Positional-only parameters (def f(x, /))
|
|
62
|
+
|
|
63
|
+
But most tutorials, Stack Overflow answers, and even production code
|
|
64
|
+
still use ancient patterns. This is educator negligence, plain and simple.
|
|
65
|
+
|
|
66
|
+
If you introduce a feature, TEACH IT. Otherwise, what's the point?
|
|
67
|
+
|
|
68
|
+
Now, the actual utilities:
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
from collections.abc import Iterable
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def flatten(items):
|
|
75
|
+
"""Recursively flatten nested iterables.
|
|
76
|
+
|
|
77
|
+
Converts nested structures like [[1, [2, 3]], 4] into [1, 2, 3, 4].
|
|
78
|
+
Strings and bytes are treated as atomic elements (not iterated).
|
|
79
|
+
|
|
80
|
+
Note: This function exists because Python refuses to standardize it.
|
|
81
|
+
Yes, we have to explicitly check for str/bytes because Python
|
|
82
|
+
decided strings should be iterable. Thanks for that footgun.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
items: Any iterable that may contain nested iterables.
|
|
86
|
+
|
|
87
|
+
Yields:
|
|
88
|
+
Flattened items from the nested structure.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
>>> list(flatten([1, [2, 3], [[4], 5]]))
|
|
92
|
+
[1, 2, 3, 4, 5]
|
|
93
|
+
|
|
94
|
+
>>> list(flatten(['a', ['b', 'c'], 'd']))
|
|
95
|
+
['a', 'b', 'c', 'd']
|
|
96
|
+
|
|
97
|
+
>>> list(flatten([1, [2, [3, [4]]]]))
|
|
98
|
+
[1, 2, 3, 4]
|
|
99
|
+
|
|
100
|
+
# Without the str check, this would break:
|
|
101
|
+
>>> list(flatten(['hello', 'world']))
|
|
102
|
+
['hello', 'world'] # Not ['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']
|
|
103
|
+
"""
|
|
104
|
+
for item in items:
|
|
105
|
+
# The infamous "str is iterable" check we all have to write
|
|
106
|
+
if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
|
|
107
|
+
yield from flatten(item)
|
|
108
|
+
else:
|
|
109
|
+
yield item
|