tano.page 0.1.0a1__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.
- tano_page-0.1.0a1/.gitignore +15 -0
- tano_page-0.1.0a1/LICENSE +5 -0
- tano_page-0.1.0a1/PKG-INFO +49 -0
- tano_page-0.1.0a1/README.md +35 -0
- tano_page-0.1.0a1/pyproject.toml +25 -0
- tano_page-0.1.0a1/src/tano_page/__init__.py +24 -0
- tano_page-0.1.0a1/src/tano_page/protocol.py +33 -0
- tano_page-0.1.0a1/src/tano_page/py.typed +0 -0
- tano_page-0.1.0a1/tests/test_protocol.py +49 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
target/
|
|
4
|
+
.DS_Store
|
|
5
|
+
*.log
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
!.env.example
|
|
9
|
+
test-results/
|
|
10
|
+
artifacts/
|
|
11
|
+
apps/client/src-tauri/gen/android/app/src/main/jniLibs/
|
|
12
|
+
apps/client/src-tauri/gen/android/app/src/main/assets/
|
|
13
|
+
apps/client/src-tauri/gen/android/app/tauri.build.gradle.kts
|
|
14
|
+
apps/client/src-tauri/gen/android/app/tauri.properties
|
|
15
|
+
data/
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 dramwig. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This package is not released under an open source license. Publication does
|
|
4
|
+
not grant permission to copy, modify, redistribute, or sublicense this work
|
|
5
|
+
except as otherwise permitted by applicable law or a separate agreement.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tano.page
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Tano Markdown workspace protocol helpers: content hashes, short IDs and portable path classification.
|
|
5
|
+
Author: dramwig
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: markdown,protocol,tano,workspace
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Tano Workspace Protocol Helpers
|
|
16
|
+
|
|
17
|
+
Tano - Humans and agents, on the same page.
|
|
18
|
+
|
|
19
|
+
This preview Python library provides working helpers for Tano's local Markdown
|
|
20
|
+
workspace protocol. It is not the desktop application or a sync client.
|
|
21
|
+
PyPI normalizes the distribution name `tano.page` to `tano-page`; import it as
|
|
22
|
+
`tano_page`.
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from tano_page import content_hash, is_short_id, is_workspace_data_path
|
|
26
|
+
|
|
27
|
+
revision = content_hash(b"# Notes\n")
|
|
28
|
+
assert len(revision) == 64
|
|
29
|
+
assert is_short_id("a1b2c3")
|
|
30
|
+
assert is_workspace_data_path(".tano/objects/a1b2c3.tano.json")
|
|
31
|
+
assert not is_workspace_data_path(".tano/config.json")
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- `content_hash` returns a lowercase SHA-256 of the exact bytes, without newline
|
|
35
|
+
or Unicode normalization.
|
|
36
|
+
- `is_short_id` recognizes six lowercase hexadecimal characters. These IDs are
|
|
37
|
+
separate from workspace UUIDs.
|
|
38
|
+
- `is_workspace_internal_path` identifies `.tano` paths.
|
|
39
|
+
- `is_workspace_data_path` classifies portable object, capability, relation,
|
|
40
|
+
and conflict paths. Workspace identity configuration is excluded.
|
|
41
|
+
|
|
42
|
+
Path classification is not path validation or filesystem authorization. Callers
|
|
43
|
+
must validate relative paths, enforce workspace boundaries, reject unsafe
|
|
44
|
+
symlinks, and compare the base hash before writing. The library performs no
|
|
45
|
+
filesystem or network operations, has no runtime dependencies, and performs
|
|
46
|
+
no actions at installation time.
|
|
47
|
+
|
|
48
|
+
Copyright dramwig. All rights reserved. See LICENSE; no open source license
|
|
49
|
+
has been granted.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Tano Workspace Protocol Helpers
|
|
2
|
+
|
|
3
|
+
Tano - Humans and agents, on the same page.
|
|
4
|
+
|
|
5
|
+
This preview Python library provides working helpers for Tano's local Markdown
|
|
6
|
+
workspace protocol. It is not the desktop application or a sync client.
|
|
7
|
+
PyPI normalizes the distribution name `tano.page` to `tano-page`; import it as
|
|
8
|
+
`tano_page`.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from tano_page import content_hash, is_short_id, is_workspace_data_path
|
|
12
|
+
|
|
13
|
+
revision = content_hash(b"# Notes\n")
|
|
14
|
+
assert len(revision) == 64
|
|
15
|
+
assert is_short_id("a1b2c3")
|
|
16
|
+
assert is_workspace_data_path(".tano/objects/a1b2c3.tano.json")
|
|
17
|
+
assert not is_workspace_data_path(".tano/config.json")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `content_hash` returns a lowercase SHA-256 of the exact bytes, without newline
|
|
21
|
+
or Unicode normalization.
|
|
22
|
+
- `is_short_id` recognizes six lowercase hexadecimal characters. These IDs are
|
|
23
|
+
separate from workspace UUIDs.
|
|
24
|
+
- `is_workspace_internal_path` identifies `.tano` paths.
|
|
25
|
+
- `is_workspace_data_path` classifies portable object, capability, relation,
|
|
26
|
+
and conflict paths. Workspace identity configuration is excluded.
|
|
27
|
+
|
|
28
|
+
Path classification is not path validation or filesystem authorization. Callers
|
|
29
|
+
must validate relative paths, enforce workspace boundaries, reject unsafe
|
|
30
|
+
symlinks, and compare the base hash before writing. The library performs no
|
|
31
|
+
filesystem or network operations, has no runtime dependencies, and performs
|
|
32
|
+
no actions at installation time.
|
|
33
|
+
|
|
34
|
+
Copyright dramwig. All rights reserved. See LICENSE; no open source license
|
|
35
|
+
has been granted.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tano.page"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Tano Markdown workspace protocol helpers: content hashes, short IDs and portable path classification."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "dramwig" }]
|
|
14
|
+
keywords = ["tano", "markdown", "workspace", "protocol"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/tano_page"]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
include = ["src/", "tests/", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Byte-level helpers for Tano's Markdown workspace protocol."""
|
|
2
|
+
|
|
3
|
+
from .protocol import (
|
|
4
|
+
OBJECT_DIRECTORY,
|
|
5
|
+
WORKSPACE_CAPABILITY_PATH,
|
|
6
|
+
WORKSPACE_DATA_DIRECTORY,
|
|
7
|
+
WORKSPACE_RELATION_PATH,
|
|
8
|
+
content_hash,
|
|
9
|
+
is_short_id,
|
|
10
|
+
is_workspace_data_path,
|
|
11
|
+
is_workspace_internal_path,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0a1"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OBJECT_DIRECTORY",
|
|
17
|
+
"WORKSPACE_CAPABILITY_PATH",
|
|
18
|
+
"WORKSPACE_DATA_DIRECTORY",
|
|
19
|
+
"WORKSPACE_RELATION_PATH",
|
|
20
|
+
"content_hash",
|
|
21
|
+
"is_short_id",
|
|
22
|
+
"is_workspace_data_path",
|
|
23
|
+
"is_workspace_internal_path",
|
|
24
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Path classification does not validate filesystem access or authorize writes."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
WORKSPACE_DATA_DIRECTORY = ".tano"
|
|
7
|
+
OBJECT_DIRECTORY = ".tano/objects"
|
|
8
|
+
WORKSPACE_CAPABILITY_PATH = ".tano/capabilities.json"
|
|
9
|
+
WORKSPACE_RELATION_PATH = ".tano/relations.tano-relations.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def content_hash(content: bytes) -> str:
|
|
13
|
+
"""Return the SHA-256 of exact bytes for Tano's expectedHash field."""
|
|
14
|
+
return hashlib.sha256(content).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def is_short_id(value: object) -> bool:
|
|
18
|
+
"""Recognize six lowercase hexadecimal characters, separate from UUIDs."""
|
|
19
|
+
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{6}", value) is not None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_workspace_internal_path(path: str) -> bool:
|
|
23
|
+
"""Classify paths inside Tano's portable data directory."""
|
|
24
|
+
return path == WORKSPACE_DATA_DIRECTORY or path.startswith(".tano/")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_workspace_data_path(path: str) -> bool:
|
|
28
|
+
"""Classify portable data paths, excluding workspace identity and app caches."""
|
|
29
|
+
return (
|
|
30
|
+
path in (WORKSPACE_CAPABILITY_PATH, WORKSPACE_RELATION_PATH)
|
|
31
|
+
or (path.startswith(".tano/conflicts/") and path.endswith(".json"))
|
|
32
|
+
or (path.startswith(".tano/objects/") and path.endswith(".tano.json"))
|
|
33
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from tano_page import (
|
|
4
|
+
WORKSPACE_CAPABILITY_PATH,
|
|
5
|
+
WORKSPACE_RELATION_PATH,
|
|
6
|
+
content_hash,
|
|
7
|
+
is_short_id,
|
|
8
|
+
is_workspace_data_path,
|
|
9
|
+
is_workspace_internal_path,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProtocolTests(unittest.TestCase):
|
|
14
|
+
def test_hashes_exact_content_bytes(self):
|
|
15
|
+
self.assertEqual(
|
|
16
|
+
content_hash(b"abc"),
|
|
17
|
+
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
|
18
|
+
)
|
|
19
|
+
self.assertNotEqual(content_hash(b"# Notes\n"), content_hash(b"# Notes\r\n"))
|
|
20
|
+
|
|
21
|
+
def test_reference_ids_do_not_accept_other_identity_formats(self):
|
|
22
|
+
for valid in ("a1b2c3", "000000"):
|
|
23
|
+
self.assertTrue(is_short_id(valid))
|
|
24
|
+
for invalid in (
|
|
25
|
+
"A1B2C3", "abcdef0", "12345", "a1b2cg", "\uff1123456", 123456,
|
|
26
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
27
|
+
):
|
|
28
|
+
with self.subTest(value=invalid):
|
|
29
|
+
self.assertFalse(is_short_id(invalid))
|
|
30
|
+
|
|
31
|
+
def test_identity_and_caches_are_excluded_from_portable_data(self):
|
|
32
|
+
for path in (
|
|
33
|
+
WORKSPACE_CAPABILITY_PATH,
|
|
34
|
+
WORKSPACE_RELATION_PATH,
|
|
35
|
+
".tano/objects/a1b2c3.tano.json",
|
|
36
|
+
".tano/conflicts/conflict.json",
|
|
37
|
+
):
|
|
38
|
+
self.assertTrue(is_workspace_internal_path(path))
|
|
39
|
+
self.assertTrue(is_workspace_data_path(path))
|
|
40
|
+
for path in (".tano/config.json", ".tano/cache.sqlite", ".tano/objects/a.json"):
|
|
41
|
+
self.assertTrue(is_workspace_internal_path(path))
|
|
42
|
+
self.assertFalse(is_workspace_data_path(path))
|
|
43
|
+
self.assertTrue(is_workspace_internal_path(".tano"))
|
|
44
|
+
self.assertFalse(is_workspace_internal_path(".tano-backup/config.json"))
|
|
45
|
+
self.assertFalse(is_workspace_internal_path("notes/.tano/config.json"))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
unittest.main()
|