lokate 1.0.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,153 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ **/__pycache__
4
+ *.py[cod]
5
+ *$py.class
6
+ *.pyc
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ pip-wheel-metadata/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py,cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+ cover/
56
+
57
+ # Translations
58
+ *.mo
59
+ *.pot
60
+
61
+ # Django stuff:
62
+ *.log
63
+ local_settings.py
64
+ db.sqlite3
65
+ db.sqlite3-journal
66
+
67
+ # Flask stuff:
68
+ instance/
69
+ .webassets-cache
70
+
71
+ # Scrapy stuff:
72
+ .scrapy
73
+
74
+ # Sphinx documentation
75
+ docs/_build/
76
+
77
+ # PyBuilder
78
+ .pybuilder/
79
+ target/
80
+
81
+ # Jupyter Notebook
82
+ .ipynb_checkpoints
83
+
84
+ # IPython
85
+ profile_default/
86
+ ipython_config.py
87
+
88
+ # pyenv
89
+ # For a library or package, you might want to ignore these files since the code is
90
+ # intended to run in multiple environments; otherwise, check them in:
91
+ # .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
101
+ __pypackages__/
102
+
103
+ # Celery stuff
104
+ celerybeat-schedule
105
+ celerybeat.pid
106
+
107
+ # SageMath parsed files
108
+ *.sage.py
109
+
110
+ # Environments
111
+ .env
112
+ .venv
113
+ env/
114
+ venv/
115
+ ENV/
116
+ env.bak/
117
+ venv.bak/
118
+
119
+ # Spyder project settings
120
+ .spyderproject
121
+ .spyproject
122
+
123
+ # Rope project settings
124
+ .ropeproject
125
+
126
+ # mkdocs documentation
127
+ /site
128
+
129
+ # mypy
130
+ .mypy_cache/
131
+ .dmypy.json
132
+ dmypy.json
133
+
134
+ # Pyre type checker
135
+ .pyre/
136
+
137
+ # pytype static type analyzer
138
+ .pytype/
139
+
140
+ # Cython debug symbols
141
+ cython_debug/
142
+
143
+ # static files generated from Django application using `collectstatic`
144
+ media
145
+ export
146
+ static_collected
147
+ data
148
+
149
+ *.token.temp
150
+ *.fakts.yaml
151
+ fakts.yaml
152
+ # Local-source override for the integration stack (see tests/conftest.py)
153
+ tests/integration/docker-compose.local.yml
lokate-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.5
2
+ Name: lokate
3
+ Version: 1.0.0
4
+ Summary: The python client for lokate, the arkitekt backup of a phone's location timeline
5
+ Author-email: jhnnsrs <jhnnsrs@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: aiohttp<4,>=3.9.5
8
+ Requires-Dist: koil>=1.0.0
9
+ Requires-Dist: rath>=4.1.1
10
+ Requires-Dist: websockets>=15.0.1
11
+ Description-Content-Type: text/markdown
12
+
13
+ # lokate
14
+
15
+ The python client for [lokate](https://github.com/arkitektio/lokate-server), the
16
+ [Arkitekt](https://arkitekt.live) backup of a phone's location timeline: points,
17
+ visits, trips and places, uploaded idempotently and restorable page by page.
18
+
19
+ ## Installation
20
+
21
+ ```sh
22
+ pip install lokate
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ Every lokate operation is a method of the `Lokate` client, in a blocking and an
28
+ `a`-prefixed async flavour. The server keys everything by the token's user and
29
+ writes by its device (`client_device` claim).
30
+
31
+ ### In an arkitekt app
32
+
33
+ The client is injected by annotation. Add the service to your app and ask for
34
+ `lokate: Lokate`:
35
+
36
+ ```python
37
+ from arkitekt import App, run
38
+ from lokate import Lokate, lokate_service
39
+
40
+ app = App("where-was-i", "0.1.0", services=[lokate_service])
41
+
42
+
43
+ @app.action
44
+ async def places_i_keep(lokate: Lokate) -> list[str]:
45
+ """The names of my places, from the server copy."""
46
+ names, cursor = [], None
47
+ while True:
48
+ page = await lokate.aget_changes(cursor=cursor)
49
+ names += [place.name for place in page.places]
50
+ cursor = page.next_cursor
51
+ if not page.has_more:
52
+ return names
53
+
54
+
55
+ if __name__ == "__main__":
56
+ run(app)
57
+ ```
58
+
59
+ ### From a script
60
+
61
+ ```python
62
+ from arkitekt import easy
63
+ from lokate import lokate_service
64
+
65
+ with easy("my-script", lokate_service) as lokate:
66
+ state = lokate.get_sync_state()
67
+ print(state.point_count, state.last_point_ts)
68
+ ```
69
+
70
+ ### Standalone
71
+
72
+ Without arkitekt, build the client over a rath link of your own:
73
+
74
+ ```python
75
+ import datetime
76
+ from lokate import Lokate
77
+ from lokate.api.schema import PointInput
78
+ from lokate.rath import LokateRath
79
+
80
+ lokate = Lokate(rath=LokateRath(link=...))
81
+
82
+ with lokate:
83
+ result = lokate.upload_points([
84
+ PointInput(client_id="fix-1", ts=datetime.datetime.now(datetime.UTC), lat=48.2, lon=16.37),
85
+ ])
86
+ print(result.accepted, result.duplicates)
87
+ ```
88
+
89
+ What the protocol guarantees:
90
+
91
+ - **Every write can be retried.** `upload_points` counts points it already has as
92
+ `duplicates`. `replace_segments(from_=...)` and `merge_places` leave unchanged
93
+ rows alone.
94
+ - **A point is keyed by `(device, client_id, ts)`.** Never send one `client_id`
95
+ with two different timestamps.
96
+ - **Batches hold at most 1000 rows**, and so does a `get_changes` page.
97
+ - **Leave defaulted arguments out** rather than passing `None`. For `set_retention`,
98
+ though, `days=None` is meaningful: it means keep forever.
99
+
100
+ ## Development
101
+
102
+ The generated API (`lokate/api/schema.py`) comes from the checked-in
103
+ `schema.graphql` and the documents in `graphql/`. The config comment in
104
+ `graphql.config.yaml` has the command that refreshes the schema from a
105
+ lokate-server checkout. Then regenerate with turms:
106
+
107
+ ```sh
108
+ uvx --with 'graphql-core<3.3' --with-editable . --from <path to turms> turms gen
109
+ ```
110
+
111
+ ```sh
112
+ uv run pytest -m "not integration" # no server needed
113
+ uv run pytest -m integration # a real lokate + postgres/PostGIS via dokker
114
+ ```
115
+
116
+ Until the server's first image is published, the integration suite needs a
117
+ gitignored `tests/integration/docker-compose.local.yml` that builds
118
+ `jhnnsrs/lokate` from a local lokate-server checkout.
119
+
120
+ See [RELEASING.md](RELEASING.md) for how versions are cut.
lokate-1.0.0/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # lokate
2
+
3
+ The python client for [lokate](https://github.com/arkitektio/lokate-server), the
4
+ [Arkitekt](https://arkitekt.live) backup of a phone's location timeline: points,
5
+ visits, trips and places, uploaded idempotently and restorable page by page.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install lokate
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Every lokate operation is a method of the `Lokate` client, in a blocking and an
16
+ `a`-prefixed async flavour. The server keys everything by the token's user and
17
+ writes by its device (`client_device` claim).
18
+
19
+ ### In an arkitekt app
20
+
21
+ The client is injected by annotation. Add the service to your app and ask for
22
+ `lokate: Lokate`:
23
+
24
+ ```python
25
+ from arkitekt import App, run
26
+ from lokate import Lokate, lokate_service
27
+
28
+ app = App("where-was-i", "0.1.0", services=[lokate_service])
29
+
30
+
31
+ @app.action
32
+ async def places_i_keep(lokate: Lokate) -> list[str]:
33
+ """The names of my places, from the server copy."""
34
+ names, cursor = [], None
35
+ while True:
36
+ page = await lokate.aget_changes(cursor=cursor)
37
+ names += [place.name for place in page.places]
38
+ cursor = page.next_cursor
39
+ if not page.has_more:
40
+ return names
41
+
42
+
43
+ if __name__ == "__main__":
44
+ run(app)
45
+ ```
46
+
47
+ ### From a script
48
+
49
+ ```python
50
+ from arkitekt import easy
51
+ from lokate import lokate_service
52
+
53
+ with easy("my-script", lokate_service) as lokate:
54
+ state = lokate.get_sync_state()
55
+ print(state.point_count, state.last_point_ts)
56
+ ```
57
+
58
+ ### Standalone
59
+
60
+ Without arkitekt, build the client over a rath link of your own:
61
+
62
+ ```python
63
+ import datetime
64
+ from lokate import Lokate
65
+ from lokate.api.schema import PointInput
66
+ from lokate.rath import LokateRath
67
+
68
+ lokate = Lokate(rath=LokateRath(link=...))
69
+
70
+ with lokate:
71
+ result = lokate.upload_points([
72
+ PointInput(client_id="fix-1", ts=datetime.datetime.now(datetime.UTC), lat=48.2, lon=16.37),
73
+ ])
74
+ print(result.accepted, result.duplicates)
75
+ ```
76
+
77
+ What the protocol guarantees:
78
+
79
+ - **Every write can be retried.** `upload_points` counts points it already has as
80
+ `duplicates`. `replace_segments(from_=...)` and `merge_places` leave unchanged
81
+ rows alone.
82
+ - **A point is keyed by `(device, client_id, ts)`.** Never send one `client_id`
83
+ with two different timestamps.
84
+ - **Batches hold at most 1000 rows**, and so does a `get_changes` page.
85
+ - **Leave defaulted arguments out** rather than passing `None`. For `set_retention`,
86
+ though, `days=None` is meaningful: it means keep forever.
87
+
88
+ ## Development
89
+
90
+ The generated API (`lokate/api/schema.py`) comes from the checked-in
91
+ `schema.graphql` and the documents in `graphql/`. The config comment in
92
+ `graphql.config.yaml` has the command that refreshes the schema from a
93
+ lokate-server checkout. Then regenerate with turms:
94
+
95
+ ```sh
96
+ uvx --with 'graphql-core<3.3' --with-editable . --from <path to turms> turms gen
97
+ ```
98
+
99
+ ```sh
100
+ uv run pytest -m "not integration" # no server needed
101
+ uv run pytest -m integration # a real lokate + postgres/PostGIS via dokker
102
+ ```
103
+
104
+ Until the server's first image is published, the integration suite needs a
105
+ gitignored `tests/integration/docker-compose.local.yml` that builds
106
+ `jhnnsrs/lokate` from a local lokate-server checkout.
107
+
108
+ See [RELEASING.md](RELEASING.md) for how versions are cut.
@@ -0,0 +1,20 @@
1
+ from .lokate import Lokate
2
+
3
+ try:
4
+ from .arkitekt import lokate as lokate_service
5
+ except ImportError as e:
6
+ # Only "rekuest is not installed" may pass silently. Anything else that fails
7
+ # to import here (a renamed query, a rekuest too old for what the module
8
+ # needs) is a bug, and hiding it makes this package's service vanish
9
+ # without a word. Whether it is installed is asked the plain way.
10
+ try:
11
+ import rekuest # noqa: F401 -- presence is the question
12
+ except ImportError:
13
+ pass
14
+ else:
15
+ raise e
16
+
17
+ __all__ = [
18
+ "Lokate",
19
+ "lokate_service",
20
+ ]
@@ -0,0 +1,120 @@
1
+ {
2
+ "schema": "schema.graphql",
3
+ "documents": "graphql/**/**.graphql",
4
+ "extensions": {
5
+ "turms": {
6
+ "domain": null,
7
+ "out_dir": "lokate/api",
8
+ "dump_configuration": true,
9
+ "configuration_name": "project.json",
10
+ "dump_schema": true,
11
+ "schema_name": "schema.graphql",
12
+ "generated_name": "schema.py",
13
+ "documents": "graphql/**/**.graphql",
14
+ "verbose": false,
15
+ "exit_on_error": true,
16
+ "allow_introspection": true,
17
+ "object_bases": [
18
+ "pydantic.BaseModel"
19
+ ],
20
+ "interface_bases": null,
21
+ "always_resolve_interfaces": true,
22
+ "exclude_typenames": true,
23
+ "scalar_definitions": {
24
+ "uuid": "str",
25
+ "Callback": "str",
26
+ "Any": "typing.Any",
27
+ "JSON": "typing.Any",
28
+ "ID": "rath.scalars.ID",
29
+ "_Any": "typing.Any",
30
+ "ByteCount": "int"
31
+ },
32
+ "coercible_scalars": {},
33
+ "coercible_inputs": {},
34
+ "graphql_default_class": null,
35
+ "deprecated_class": null,
36
+ "document_field_metadata": true,
37
+ "unset_type_class": null,
38
+ "unset_instance": null,
39
+ "freeze": {
40
+ "enabled": true,
41
+ "types": [
42
+ "input",
43
+ "fragment",
44
+ "object"
45
+ ],
46
+ "exclude": null,
47
+ "include": null,
48
+ "convert_list_to_tuple": true
49
+ },
50
+ "create_catchall": true,
51
+ "options": {
52
+ "enabled": true,
53
+ "extra": "forbid",
54
+ "populate_by_name": true,
55
+ "from_attributes": null,
56
+ "use_enum_values": true,
57
+ "validate_assignment": null,
58
+ "alias_mode": "split",
59
+ "types": [
60
+ "input"
61
+ ],
62
+ "exclude": null,
63
+ "include": null
64
+ },
65
+ "skip_forwards": false,
66
+ "min_python_version": "3.10",
67
+ "type_annotation_style": "auto",
68
+ "external_modules": [],
69
+ "additional_bases": {},
70
+ "additional_config": {},
71
+ "force_plugin_order": true,
72
+ "omitted_document_rules": [],
73
+ "parsers": [],
74
+ "plugins": [
75
+ {
76
+ "type": "turms.plugins.enums.EnumsPlugin"
77
+ },
78
+ {
79
+ "type": "turms.plugins.inputs.InputsPlugin"
80
+ },
81
+ {
82
+ "type": "turms.plugins.fragments.FragmentsPlugin"
83
+ },
84
+ {
85
+ "type": "turms.plugins.operations.OperationsPlugin"
86
+ },
87
+ {
88
+ "type": "turms.plugins.client.ClientPlugin",
89
+ "client_class": "LokateApi",
90
+ "global_kwargs": [
91
+ {
92
+ "key": "task",
93
+ "type": "rath.task.TaskLike",
94
+ "description": "The task this call is made for; the ambient one by default."
95
+ }
96
+ ],
97
+ "reserved_from": [
98
+ "lokate.lokate.Lokate"
99
+ ],
100
+ "coercible_scalars": {
101
+ "ID": "rath.scalars.IDCoercible"
102
+ },
103
+ "expand_input_types": [
104
+ "input"
105
+ ]
106
+ }
107
+ ],
108
+ "processors": [],
109
+ "stylers": [
110
+ {
111
+ "type": "turms.stylers.default.DefaultStyler"
112
+ },
113
+ {
114
+ "type": "turms.stylers.appender.AppenderStyler",
115
+ "append_fragment": ""
116
+ }
117
+ ]
118
+ }
119
+ }
120
+ }