kystdatahuset-python-lib 0.9.7__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.

Potentially problematic release.


This version of kystdatahuset-python-lib might be problematic. Click here for more details.

Files changed (33) hide show
  1. kystdatahuset_python_lib-0.9.7/LICENSE +21 -0
  2. kystdatahuset_python_lib-0.9.7/PKG-INFO +144 -0
  3. kystdatahuset_python_lib-0.9.7/README.md +117 -0
  4. kystdatahuset_python_lib-0.9.7/pyproject.toml +55 -0
  5. kystdatahuset_python_lib-0.9.7/setup.cfg +4 -0
  6. kystdatahuset_python_lib-0.9.7/src/__init__.py +0 -0
  7. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/__init__.py +1 -0
  8. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/ais.py +29 -0
  9. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/api_client.py +103 -0
  10. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/auth.py +30 -0
  11. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/const.py +1 -0
  12. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/file_storage.py +54 -0
  13. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/logging.py +13 -0
  14. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/models/AuthData.py +10 -0
  15. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/models/FileListing.py +33 -0
  16. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/models/WebServiceResponse.py +11 -0
  17. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/models/__init__.py +3 -0
  18. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/types/PandasFrequency.py +8 -0
  19. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/types/UploadFileType.py +18 -0
  20. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/types/__init__.py +2 -0
  21. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/utils/__init__.py +2 -0
  22. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/utils/_date_range.py +15 -0
  23. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/utils/_slice_polygon_to_grid.py +48 -0
  24. kystdatahuset_python_lib-0.9.7/src/kystdatahuset/voyage.py +45 -0
  25. kystdatahuset_python_lib-0.9.7/src/kystdatahuset_python_lib.egg-info/PKG-INFO +144 -0
  26. kystdatahuset_python_lib-0.9.7/src/kystdatahuset_python_lib.egg-info/SOURCES.txt +31 -0
  27. kystdatahuset_python_lib-0.9.7/src/kystdatahuset_python_lib.egg-info/dependency_links.txt +1 -0
  28. kystdatahuset_python_lib-0.9.7/src/kystdatahuset_python_lib.egg-info/requires.txt +6 -0
  29. kystdatahuset_python_lib-0.9.7/src/kystdatahuset_python_lib.egg-info/top_level.txt +2 -0
  30. kystdatahuset_python_lib-0.9.7/tests/test_ais.py +18 -0
  31. kystdatahuset_python_lib-0.9.7/tests/test_auth.py +8 -0
  32. kystdatahuset_python_lib-0.9.7/tests/test_file_storage.py +24 -0
  33. kystdatahuset_python_lib-0.9.7/tests/test_voyage.py +16 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kystverket
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,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: kystdatahuset-python-lib
3
+ Version: 0.9.7
4
+ Summary: A python library for accessing and querying data from Kystdatahuset
5
+ Author-email: Kystdatahuset developer team <support@kystdatahuset.no>, "(Stein) Runar Bergheim" <runar.bergheim@avinet.no>, Sigve Bergh <sigve.bergh@kystverket.no>, Hermann Klaus Kurt von Lupfert <hermann.lupfert@kystverket.no>
6
+ Maintainer-email: Kystdatahuset developer team <support@kystdatahuset.no>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/Kystverket/kystdatahuset-python-lib
9
+ Project-URL: Repository, https://github.com/Kystverket/kystdatahuset-python-lib
10
+ Project-URL: Issues, https://github.com/Kystverket/kystdatahuset-python-lib/issues
11
+ Keywords: spatial,analytics,visualization,geodata
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: pandas>=2.0
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: requests>=2.28
24
+ Requires-Dist: shapely>=2.0
25
+ Requires-Dist: tqdm>=4.65
26
+ Dynamic: license-file
27
+
28
+ # Kystdatahuset Python Library
29
+
30
+ `kystdatahuset-python-lib` — Python SDK companion for the Kystdatahuset API
31
+
32
+ `kystdatahuset-python-lib` is the official Python client for accessing the **Kystdatahuset API**, a unified data and knowledge platform for coastal and maritime spatial analytics.
33
+
34
+ It provides a clean, Pythonic, and strongly typed interface for querying datasets, managing authentication, and performing efficient data access.
35
+
36
+ ---
37
+
38
+ ## ✨ Features
39
+
40
+ ### 🚀 Easy Installation
41
+ Install directly from PyPI:
42
+
43
+ ```bash
44
+ pip install kystdatahuset-python-lib
45
+ ```
46
+
47
+ Supports Python **3.9+** on Linux, macOS, and Windows.
48
+
49
+ ---
50
+
51
+ ### 🔐 Simple Authentication
52
+ The client offers:
53
+
54
+ - API key authentication
55
+ - Support for headless servers and notebooks
56
+
57
+ Example:
58
+
59
+ ```python
60
+ from kystdatahuset.auth import login
61
+ import os
62
+
63
+ login_response = login("username", "password")
64
+ jwt = auth_res.data.JWT
65
+ voyages = get_voyages_for_ships_by_mmsi(
66
+ auth_jwt=jwt,
67
+ mmsi_ids=[258090000, 259028000],
68
+ start_date=datetime(2024,1,1),
69
+ end_date=datetime(2024,5,1),
70
+ )
71
+
72
+ ```
73
+ ---
74
+
75
+ ## 🌍 Efficient & “Social” Data Access
76
+
77
+ Instead of fetching massive multi-GB extracts, the library is designed for **smart, cooperative usage patterns**, where users share infrastructure responsibly:
78
+
79
+ ### ✅ Time Window Batching
80
+ Fetch long time periods in small, safe slices, python/Pandas "periods"
81
+
82
+
83
+ ### ✅ Geographic Slicing
84
+ Request only the needed spatial extent by WKT filters
85
+
86
+ ---
87
+
88
+ ## 🧱 Library Structure
89
+
90
+ ```
91
+ +---kystdatahuset
92
+ | | ais.py
93
+ | | api_client.py
94
+ | | auth.py
95
+ | | const.py
96
+ | | file_storage.py
97
+ | | logging.py
98
+ | | voyage.py
99
+ | | __init__.py
100
+ | |
101
+ | +---models
102
+ | | | AuthData.py
103
+ | | | FileListing.py
104
+ | | | WebServiceResponse.py
105
+ | | | __init__.py
106
+ | | |
107
+ | |
108
+ | +---types
109
+ | | | PandasFrequency.py
110
+ | | | UploadFileType.py
111
+ | | | __init__.py
112
+ | | |
113
+ | |
114
+ | +---utils
115
+ | | | _date_range.py
116
+ | | | __init__.py
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 📦 Development & Distribution
122
+
123
+ `kystdatahuset-py` uses standard packaging:
124
+
125
+ - `pyproject.toml` + `PEP 621` metadata
126
+ - versioning via Semantic Versioning
127
+ - full type hints (mypy-friendly)
128
+ - GitHub Actions for automated testing & publishing
129
+
130
+ ---
131
+
132
+ ## 🧠 Typical Use Cases
133
+
134
+ - Query live AIS vessel data efficiently
135
+ - Retrieve spatial datasets in bounded windows
136
+ - Build dashboards, decision-support tools, or AI/ML pipelines
137
+ - Avoid oversized extracts by using time/space batching helpers
138
+
139
+ ---
140
+
141
+ ## 📄 License
142
+
143
+ Open source under the **MIT License**.
144
+
@@ -0,0 +1,117 @@
1
+ # Kystdatahuset Python Library
2
+
3
+ `kystdatahuset-python-lib` — Python SDK companion for the Kystdatahuset API
4
+
5
+ `kystdatahuset-python-lib` is the official Python client for accessing the **Kystdatahuset API**, a unified data and knowledge platform for coastal and maritime spatial analytics.
6
+
7
+ It provides a clean, Pythonic, and strongly typed interface for querying datasets, managing authentication, and performing efficient data access.
8
+
9
+ ---
10
+
11
+ ## ✨ Features
12
+
13
+ ### 🚀 Easy Installation
14
+ Install directly from PyPI:
15
+
16
+ ```bash
17
+ pip install kystdatahuset-python-lib
18
+ ```
19
+
20
+ Supports Python **3.9+** on Linux, macOS, and Windows.
21
+
22
+ ---
23
+
24
+ ### 🔐 Simple Authentication
25
+ The client offers:
26
+
27
+ - API key authentication
28
+ - Support for headless servers and notebooks
29
+
30
+ Example:
31
+
32
+ ```python
33
+ from kystdatahuset.auth import login
34
+ import os
35
+
36
+ login_response = login("username", "password")
37
+ jwt = auth_res.data.JWT
38
+ voyages = get_voyages_for_ships_by_mmsi(
39
+ auth_jwt=jwt,
40
+ mmsi_ids=[258090000, 259028000],
41
+ start_date=datetime(2024,1,1),
42
+ end_date=datetime(2024,5,1),
43
+ )
44
+
45
+ ```
46
+ ---
47
+
48
+ ## 🌍 Efficient & “Social” Data Access
49
+
50
+ Instead of fetching massive multi-GB extracts, the library is designed for **smart, cooperative usage patterns**, where users share infrastructure responsibly:
51
+
52
+ ### ✅ Time Window Batching
53
+ Fetch long time periods in small, safe slices, python/Pandas "periods"
54
+
55
+
56
+ ### ✅ Geographic Slicing
57
+ Request only the needed spatial extent by WKT filters
58
+
59
+ ---
60
+
61
+ ## 🧱 Library Structure
62
+
63
+ ```
64
+ +---kystdatahuset
65
+ | | ais.py
66
+ | | api_client.py
67
+ | | auth.py
68
+ | | const.py
69
+ | | file_storage.py
70
+ | | logging.py
71
+ | | voyage.py
72
+ | | __init__.py
73
+ | |
74
+ | +---models
75
+ | | | AuthData.py
76
+ | | | FileListing.py
77
+ | | | WebServiceResponse.py
78
+ | | | __init__.py
79
+ | | |
80
+ | |
81
+ | +---types
82
+ | | | PandasFrequency.py
83
+ | | | UploadFileType.py
84
+ | | | __init__.py
85
+ | | |
86
+ | |
87
+ | +---utils
88
+ | | | _date_range.py
89
+ | | | __init__.py
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 📦 Development & Distribution
95
+
96
+ `kystdatahuset-py` uses standard packaging:
97
+
98
+ - `pyproject.toml` + `PEP 621` metadata
99
+ - versioning via Semantic Versioning
100
+ - full type hints (mypy-friendly)
101
+ - GitHub Actions for automated testing & publishing
102
+
103
+ ---
104
+
105
+ ## 🧠 Typical Use Cases
106
+
107
+ - Query live AIS vessel data efficiently
108
+ - Retrieve spatial datasets in bounded windows
109
+ - Build dashboards, decision-support tools, or AI/ML pipelines
110
+ - Avoid oversized extracts by using time/space batching helpers
111
+
112
+ ---
113
+
114
+ ## 📄 License
115
+
116
+ Open source under the **MIT License**.
117
+
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kystdatahuset-python-lib"
7
+ dynamic = ["version"]
8
+ description = "A python library for accessing and querying data from Kystdatahuset"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [
13
+ { name="Kystdatahuset developer team", email="support@kystdatahuset.no" },
14
+ { name="(Stein) Runar Bergheim", email="runar.bergheim@avinet.no" },
15
+ { name="Sigve Bergh", email="sigve.bergh@kystverket.no" },
16
+ { name="Hermann Klaus Kurt von Lupfert", email="hermann.lupfert@kystverket.no" },
17
+ ]
18
+ maintainers = [
19
+ { name="Kystdatahuset developer team", email="support@kystdatahuset.no" },
20
+ ]
21
+
22
+ classifiers = [
23
+ # 3 - Alpha
24
+ # 4 - Beta
25
+ # 5 - Production/Stable
26
+ "Development Status :: 4 - Beta",
27
+ # Indicate who your project is intended for
28
+ "Intended Audience :: Developers",
29
+ "Topic :: Software Development :: Libraries",
30
+ "Topic :: Scientific/Engineering :: Information Analysis",
31
+ # Specify the Python versions you support here.
32
+ "Programming Language :: Python :: 3.10",
33
+ ]
34
+ license-files = ["LICEN[CS]E*"]
35
+ keywords = ["spatial", "analytics", "visualization", "geodata"]
36
+ dependencies = [
37
+ "numpy>=1.24",
38
+ "pandas>=2.0",
39
+ "pydantic>=2.0",
40
+ "requests>=2.28",
41
+ "shapely>=2.0",
42
+ "tqdm>=4.65",
43
+ ]
44
+
45
+ [project.urls]
46
+ Homepage = "https://github.com/Kystverket/kystdatahuset-python-lib"
47
+ Repository = "https://github.com/Kystverket/kystdatahuset-python-lib"
48
+ Issues = "https://github.com/Kystverket/kystdatahuset-python-lib/issues"
49
+
50
+ [tool.pytest.ini_options]
51
+ pythonpath = ["src"]
52
+ testpaths = ["tests"]
53
+
54
+ [tool.setuptools.dynamic]
55
+ version = {attr = "kystdatahuset.__version__"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1 @@
1
+ __version__ = "0.9.7"
@@ -0,0 +1,29 @@
1
+ from datetime import datetime
2
+ from typing import Dict, List
3
+ from kystdatahuset.api_client import post_api
4
+
5
+ def get_ais_positions_within_geom_time(
6
+ jwt_token: str,
7
+ geometry: str,
8
+ start_time: datetime,
9
+ end_time: datetime,
10
+ ) -> List[Dict]:
11
+ """
12
+ Placeholder for getting AIS positions for a given geometry and time range.
13
+ """
14
+ response = post_api(
15
+ jwt_token=jwt_token,
16
+ fragment="api/ais/positions/within-geom-time",
17
+ payload={
18
+ "geom": geometry,
19
+ "start": start_time.isoformat(),
20
+ "end": end_time.isoformat(),
21
+ "minSpeed": 0,
22
+ },
23
+ )
24
+
25
+ # This is a placeholder implementation.
26
+ if (response is None) or (not response["success"]):
27
+ raise Exception(f"Failed to get AIS positions: {response['msg'] if response else 'No response'}")
28
+
29
+ return response["data"]
@@ -0,0 +1,103 @@
1
+ from typing import Any, Dict, List, Tuple, Optional
2
+
3
+ from kystdatahuset.models import WebServiceResponse
4
+ from .const import API_URL
5
+ import requests
6
+ from kystdatahuset.logging import logger
7
+
8
+ def get_headers(jwt_token: Optional[str] = None, json: bool = False) -> Dict[str, str]:
9
+ """
10
+ Build headers for API requests.
11
+ """
12
+ headers: Dict[str, str] = {}
13
+ if jwt_token:
14
+ headers["Authorization"] = f"Bearer {jwt_token}"
15
+ if json:
16
+ headers["Content-Type"] = "application/json"
17
+ headers["Accept"] = "application/json"
18
+ return headers
19
+
20
+
21
+ def post_api_formdata(jwt_token: str, fragment: str, data: List[Tuple[str, Any]], filename: str) -> WebServiceResponse:
22
+ """
23
+ POST multipart/form-data with a file and form fields.
24
+ """
25
+ url = f"{API_URL}/{fragment}"
26
+
27
+ with open(filename, "rb") as f:
28
+ files = {"file": (filename, f, "application/octet-stream")}
29
+ response = requests.post(url, headers=get_headers(jwt_token), data=data, files=files)
30
+
31
+ if response.ok:
32
+ try:
33
+ logger.info(f"✅ POST (FormData) {url} successful!")
34
+ data = response.json()
35
+ return data
36
+ except ValueError:
37
+ return WebServiceResponse[Any](**{"success": True, "msg": response.text, "data": None})
38
+ else:
39
+ logger.error(f"❌ POST (FormData) {url} failed with {response.status_code}")
40
+ logger.debug(response.text)
41
+ raise Exception(f"API POST (FormData) failed with status code {response.status_code}: {response.text}")
42
+
43
+
44
+ def get_api(jwt_token: str, fragment: str, params: Optional[Dict[str, Any]] = None) -> WebServiceResponse:
45
+ """
46
+ Perform a GET request and parse JSON response.
47
+ """
48
+ url = f"{API_URL}/{fragment}"
49
+ response = requests.get(url, headers=get_headers(jwt_token, json=True), params=params)
50
+
51
+ if response.ok:
52
+ logger.info(f"✅ GET {url} successful!")
53
+ try:
54
+ data = response.json()
55
+ logger.debug(f"Response JSON: {data}")
56
+ return data
57
+ except ValueError:
58
+ return WebServiceResponse[str](**{"success": True, "msg": response.text, "data": None})
59
+ else:
60
+ logger.error(f"❌ GET {url} failed with {response.status_code}")
61
+ logger.debug(response.text)
62
+ raise Exception(f"API GET failed with status code {response.status_code}: {response.text}")
63
+
64
+ def delete_api(jwt_token: str, fragment: str, params: Optional[Dict[str, Any]] = None) -> WebServiceResponse:
65
+ """
66
+ Perform a DELETE request and parse the JSON response.
67
+ """
68
+ url = f"{API_URL}/{fragment}"
69
+ response = requests.delete(url, headers=get_headers(jwt_token, json=True), params=params)
70
+
71
+ if response.ok:
72
+ logger.info(f"✅ DELETE {url} successful!")
73
+ try:
74
+ data = response.json()
75
+ logger.debug(f"Response JSON: {data}")
76
+ return data
77
+ except ValueError:
78
+ return WebServiceResponse[str](**{"success": True, "msg": response.text, "data": None})
79
+ else:
80
+ logger.error(f"❌ GET {url} failed with {response.status_code}")
81
+ logger.debug(response.text)
82
+ raise Exception(f"API GET failed with status code {response.status_code}: {response.text}")
83
+
84
+
85
+ def post_api(jwt_token: str, fragment: str, payload: Dict[str, Any]) -> WebServiceResponse:
86
+ """
87
+ Perform a POST request with a JSON body and parse JSON response.
88
+ """
89
+ url = f"{API_URL}/{fragment}"
90
+ response = requests.post(url, headers=get_headers(jwt_token, json=True), json=payload)
91
+
92
+ if response.ok:
93
+ logger.info(f"✅ JSON POST to {url} successful!")
94
+ try:
95
+ data = response.json()
96
+ logger.debug(f"Response JSON: {data}")
97
+ return data
98
+ except ValueError:
99
+ return WebServiceResponse[str](**{"success": True, "message": response.text, "data": None})
100
+ else:
101
+ logger.error(f"❌ JSON POST to {url} failed with {response.status_code}")
102
+ logger.debug(response.text)
103
+ raise Exception(f"API JSON POST failed with status code {response.status_code}: {response.text}")
@@ -0,0 +1,30 @@
1
+ import requests
2
+ import json
3
+
4
+ from kystdatahuset.models import AuthData, WebServiceResponse
5
+ from kystdatahuset.logging import logger
6
+ from .const import API_URL
7
+
8
+ def login(username: str, password: str) -> WebServiceResponse[AuthData]:
9
+ reqUrl = f"{API_URL}/api/auth/login"
10
+
11
+ headersList = {
12
+ "User-Agent": "Kystdatahuset Python Library (https://your-client.com)",
13
+ "accept": "*/*",
14
+ "Content-Type": "application/json"
15
+ }
16
+
17
+ payload = json.dumps({
18
+ "username": username,
19
+ "password": password
20
+ })
21
+
22
+ response = requests.request("POST", reqUrl, data=payload, headers=headersList)
23
+
24
+ if response.status_code == 200:
25
+ logger.info("✅ Login successful!")
26
+ return WebServiceResponse[AuthData](**response.json())
27
+ else:
28
+ logger.error(f"❌ Login failed with status code {response.status_code}")
29
+ logger.debug(response.text)
30
+ raise Exception(f"Login failed with status code {response.status_code}: {response.text}")
@@ -0,0 +1 @@
1
+ API_URL="https://kystdatahuset.no/ws"
@@ -0,0 +1,54 @@
1
+ from uuid import UUID
2
+ from typing import Sequence, List
3
+ from kystdatahuset.models import FileListing, WebServiceResponse
4
+ from kystdatahuset.types import UploadFileType
5
+ import os
6
+ from kystdatahuset.api_client import post_api_formdata, get_api, delete_api
7
+
8
+ def list(*, jwt_token: str, resource_uuid: UUID) -> List[FileListing]:
9
+ """
10
+ Placeholder for listing files in storage.
11
+ """
12
+ list_res = get_api(jwt_token, f"api/file-storage/list/{resource_uuid}")
13
+ file_listings = WebServiceResponse[List[FileListing]](**list_res)
14
+ return file_listings.data
15
+
16
+ def delete(* , jwt_token: str, file_uuid: UUID) -> bool:
17
+ """
18
+ Placeholder for deleting a file in storage.
19
+ """
20
+ delete_res = delete_api(jwt_token, f"api/file-storage/delete/{file_uuid}")
21
+ return delete_res.success
22
+
23
+ def publish(
24
+ *,
25
+ jwt_token: str,
26
+ resource_uuid: UUID,
27
+ file_path: str,
28
+ title: str,
29
+ upload_file_type: UploadFileType,
30
+ description: str = "",
31
+ categories: Sequence[str] = "",
32
+ compressed: bool = False,
33
+ ) -> bool:
34
+ """
35
+ Upload a file and metadata to the Kystdatahuset API.
36
+ """
37
+
38
+ if not os.path.exists(file_path):
39
+ raise FileNotFoundError(f"File not found: {file_path}")
40
+
41
+ # Convert categories to multiple form fields (ASP.NET supports repeated keys)
42
+ # or as a JSON-like string, depending on the API’s binding expectations.
43
+ # The safe bet for ASP.NET [FromForm(Name="categories")] string[] is to repeat the key.
44
+ data = [
45
+ ("title", title),
46
+ ("description", description),
47
+ ("type", upload_file_type),
48
+ ("compressed", str(compressed).lower()), # ASP.NET expects 'true'/'false'
49
+ ] + [("categories", c) for c in categories]
50
+
51
+ response = post_api_formdata(jwt_token, f"api/file-storage/publish/{resource_uuid}", data, file_path)
52
+ return response.success
53
+
54
+
@@ -0,0 +1,13 @@
1
+ import logging
2
+
3
+ logger_name = __name__.split(".")[0]
4
+ # Create a logger specific to your library
5
+ logger = logging.getLogger(logger_name)
6
+ logger.addHandler(logging.NullHandler())
7
+
8
+ def enable_default_logging(level=logging.INFO):
9
+ handler = logging.StreamHandler()
10
+ formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
11
+ handler.setFormatter(formatter)
12
+ logger.addHandler(handler)
13
+ logger.setLevel(level)
@@ -0,0 +1,10 @@
1
+ from pydantic import BaseModel, Field
2
+ from datetime import datetime
3
+
4
+
5
+ class AuthData(BaseModel):
6
+ JWT: str = Field(..., description="JSON Web Token for authentication")
7
+ Username: str = Field(..., description="User's email or username")
8
+ Timestamp: datetime = Field(..., description="Timestamp when the token was issued")
9
+
10
+
@@ -0,0 +1,33 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import List
3
+ from uuid import UUID
4
+
5
+
6
+ class FileListing(BaseModel):
7
+ uuid: UUID = Field(..., description="Unique identifier for this file record")
8
+ title: str = Field(..., description="Title or display name of the uploaded file")
9
+ description: str = Field(..., description="Descriptive text about the file contents")
10
+ categories: List[str] = Field(..., description="List of category tags assigned to the file")
11
+ filetype: str = Field(..., description="Type or format of the uploaded file, e.g. csv, pdf")
12
+ compressed: bool = Field(..., description="True if the file was uploaded as compressed archive")
13
+ filename: str = Field(..., description="Server-side absolute file path")
14
+ origFilename: str = Field(..., description="Original filename on the client before upload")
15
+ resourceUuid: UUID = Field(..., description="UUID of the resource to which this file belongs")
16
+
17
+
18
+ # # ✅ Example usage:
19
+ # example_json = {
20
+ # 'uuid': '69d47723-206c-4a89-8d67-654f23706e24',
21
+ # 'title': 'Test Upload',
22
+ # 'description': 'This is a test upload',
23
+ # 'categories': ['test', 'upload'],
24
+ # 'filetype': 'csv',
25
+ # 'compressed': True,
26
+ # 'filename': r'E:\storage\kystdathuset\catalog\2025\11\64\12\7f\64127fc2-2644-4ed9-b886-fecfb914c4b5\C_Users_runar.bergheim_Documents_Development_kystdatahuset-python-lib_test_data_content.txt',
27
+ # 'origFilename': r'C:\\Users\\runar.bergheim\\Documents\\Development\\kystdatahuset-python-lib\\test_data\\content.txt',
28
+ # 'resourceUuid': '64127fc2-2644-4ed9-b886-fecfb914c4b5'
29
+ # }
30
+
31
+ # listing = FileListing(**example_json)
32
+ # print(listing.title)
33
+ # print(listing.resourceUuid)
@@ -0,0 +1,11 @@
1
+ from pydantic import Field, BaseModel
2
+ from typing import Generic, TypeVar, Optional
3
+
4
+ T = TypeVar("T")
5
+
6
+ class WebServiceResponse(BaseModel, Generic[T]):
7
+ success: bool = Field(..., description="Indicates if the request was successful")
8
+ msg: str = Field(..., description="Optional message from the API")
9
+ data: Optional[T] = Field(None, description="Container for typed response data")
10
+ time: Optional[float] = Field(None, description="Processing time in milliseconds or seconds")
11
+ executionTime: Optional[float] = Field(None, description="Processing time in milliseconds or seconds")
@@ -0,0 +1,3 @@
1
+ from .AuthData import AuthData
2
+ from .WebServiceResponse import WebServiceResponse
3
+ from .FileListing import FileListing
@@ -0,0 +1,8 @@
1
+ from typing import Literal, TypeAlias
2
+
3
+ PandasFreqency: TypeAlias = Literal[
4
+ "B", "C", "D", "W", "W-MON", "W-TUE", "W-WED", "W-THU", "W-FRI", "W-SAT", "W-SUN",
5
+ "M", "MS", "Q", "QS", "A", "AS",
6
+ "H", "T", "min", "S", "L", "ms", "U", "us", "N",
7
+ "BH", "CBH", "BQS", "BA", "BAS", "BYS", "BY"
8
+ ]
@@ -0,0 +1,18 @@
1
+ from typing import Literal, TypeAlias
2
+
3
+ UploadFileType: TypeAlias = Literal[
4
+ "csv", # Comma Separated Variables
5
+ "shp-compressed", # Compressed ESRI Shapefile
6
+ "fgdb-compressed", # Compressed ESRI filegeodatabase
7
+ "json", # JSON file
8
+ "geojson", # GeoJSON file
9
+ "png", # Portable Network Graphics image
10
+ "jpg", # JPEG image
11
+ "pdf", # Portable Document Format
12
+ "xlsx", # Microsoft Excel spreadsheet
13
+ "xml", # XML file
14
+ "docx", # Microsoft Word document
15
+ "NetCDF", # Network Common Data Form (NetCDF)
16
+ "tiff", # Tagged Image File Format (*.tif)
17
+ "geotiff", # Georeferenced Tagged Image File Format (*.tif)
18
+ ]
@@ -0,0 +1,2 @@
1
+ from .PandasFrequency import PandasFreqency
2
+ from .UploadFileType import UploadFileType
@@ -0,0 +1,2 @@
1
+ from ._date_range import date_range
2
+ from ._slice_polygon_to_grid import slice_polygon_to_grid
@@ -0,0 +1,15 @@
1
+ from typing import List
2
+ import pandas as pd
3
+ from datetime import datetime
4
+ from kystdatahuset.types import PandasFreqency
5
+ from more_itertools import pairwise
6
+
7
+ def date_range(start_date: datetime, end_date: datetime, freq: PandasFreqency = "D") -> List[datetime]:
8
+ """
9
+ Generate a list of dates from start_date to end_date, inclusive.
10
+ """
11
+ if start_date > end_date:
12
+ raise ValueError("start_date must be less than or equal to end_date")
13
+
14
+ dates = pd.date_range(start=start_date, end=end_date, freq=freq)
15
+ return pairwise([dt.to_pydatetime() for dt in dates])
@@ -0,0 +1,48 @@
1
+ from shapely.geometry import Polygon, box
2
+ from shapely.ops import unary_union
3
+ import numpy as np
4
+ from typing import List, Tuple
5
+ from shapely import wkt
6
+
7
+ def slice_polygon_to_grid(
8
+ wkt_polygon: str,
9
+ grid_size: float,
10
+ bbox: Tuple[float, float, float, float] = None
11
+ ) -> List[str]:
12
+ """
13
+ Slice a large polygon into smaller pieces that fit a regular grid.
14
+
15
+ Args:
16
+ wkt_polygon (str): The well-known text representation of the polygon to be sliced.
17
+ grid_size (float): The grid cell size (in same units as polygon coordinates).
18
+ bbox (tuple, optional): (minx, miny, maxx, maxy) to constrain the grid.
19
+ If not provided, the polygon's bounding box is used.
20
+
21
+ Returns:
22
+ List[str]: List of wkt polygon strings (each is the intersection of the polygon and one grid cell).
23
+ """
24
+
25
+ polygon = wkt.loads(wkt_polygon)
26
+
27
+ if bbox is None:
28
+ minx, miny, maxx, maxy = polygon.bounds
29
+ else:
30
+ minx, miny, maxx, maxy = bbox
31
+
32
+ # Create grid coordinates
33
+ x_coords = np.arange(minx, maxx, grid_size)
34
+ y_coords = np.arange(miny, maxy, grid_size)
35
+
36
+ pieces = []
37
+ for x in x_coords:
38
+ for y in y_coords:
39
+ cell = box(x, y, x + grid_size, y + grid_size)
40
+ intersection = polygon.intersection(cell)
41
+ if not intersection.is_empty:
42
+ # Handle MultiPolygons (split into individual polygons)
43
+ if intersection.geom_type == "Polygon":
44
+ pieces.append(intersection)
45
+ elif intersection.geom_type == "MultiPolygon":
46
+ pieces.extend(intersection.geoms)
47
+
48
+ return [wkt.dumps(p) for p in pieces]
@@ -0,0 +1,45 @@
1
+ from typing import List, Dict
2
+ from requests_cache import datetime
3
+ from kystdatahuset.api_client import post_api
4
+ from kystdatahuset.types import PandasFrequency
5
+ from kystdatahuset.utils import date_range
6
+
7
+ def get_voyages_for_ships_by_mmsi(auth_jwt: str, mmsi_ids: List[int], start_date: datetime, end_date: datetime, freq: PandasFrequency = "MS") -> List[Dict]:
8
+ """Get voyagen data for ships identified by MMSI ids
9
+
10
+ Args:
11
+ auth_jwt (str): A valid JWT retrieved through an authentication call to the API
12
+ mmsi_ids (List[int]): A list of one or more MMSIs
13
+ start_date (datetime): A start date
14
+ end_date (datetime): An end date
15
+ freq (PandasFrequency, optional): An optional frequency that the request will be split into. Defaults to "MS".
16
+
17
+ Raises:
18
+ Exception: If the API call fails or returns an error.
19
+
20
+ Returns:
21
+ List[Dict]: List of voyages
22
+ """
23
+ responses = []
24
+
25
+ date_ranges = date_range(start_date, end_date, freq)
26
+ for pair in date_ranges:
27
+ print(pair) # Debug print to verify date ranges
28
+
29
+ response = post_api(
30
+ jwt_token=auth_jwt,
31
+ fragment="api/voyage/for-ships/by-mmsi",
32
+ payload={
33
+ "mmsiIds": mmsi_ids,
34
+ "startTime": start_date.isoformat(),
35
+ "endTime": end_date.isoformat()
36
+ },
37
+ )
38
+
39
+ if (response is None) or (not response["success"]):
40
+ raise Exception(f"Failed to get voyages for ships by MMSI: {response['msg'] if response else 'No response'}")
41
+ else:
42
+ responses.extend(response["data"])
43
+
44
+ return responses
45
+
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: kystdatahuset-python-lib
3
+ Version: 0.9.7
4
+ Summary: A python library for accessing and querying data from Kystdatahuset
5
+ Author-email: Kystdatahuset developer team <support@kystdatahuset.no>, "(Stein) Runar Bergheim" <runar.bergheim@avinet.no>, Sigve Bergh <sigve.bergh@kystverket.no>, Hermann Klaus Kurt von Lupfert <hermann.lupfert@kystverket.no>
6
+ Maintainer-email: Kystdatahuset developer team <support@kystdatahuset.no>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/Kystverket/kystdatahuset-python-lib
9
+ Project-URL: Repository, https://github.com/Kystverket/kystdatahuset-python-lib
10
+ Project-URL: Issues, https://github.com/Kystverket/kystdatahuset-python-lib/issues
11
+ Keywords: spatial,analytics,visualization,geodata
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries
15
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: pandas>=2.0
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: requests>=2.28
24
+ Requires-Dist: shapely>=2.0
25
+ Requires-Dist: tqdm>=4.65
26
+ Dynamic: license-file
27
+
28
+ # Kystdatahuset Python Library
29
+
30
+ `kystdatahuset-python-lib` — Python SDK companion for the Kystdatahuset API
31
+
32
+ `kystdatahuset-python-lib` is the official Python client for accessing the **Kystdatahuset API**, a unified data and knowledge platform for coastal and maritime spatial analytics.
33
+
34
+ It provides a clean, Pythonic, and strongly typed interface for querying datasets, managing authentication, and performing efficient data access.
35
+
36
+ ---
37
+
38
+ ## ✨ Features
39
+
40
+ ### 🚀 Easy Installation
41
+ Install directly from PyPI:
42
+
43
+ ```bash
44
+ pip install kystdatahuset-python-lib
45
+ ```
46
+
47
+ Supports Python **3.9+** on Linux, macOS, and Windows.
48
+
49
+ ---
50
+
51
+ ### 🔐 Simple Authentication
52
+ The client offers:
53
+
54
+ - API key authentication
55
+ - Support for headless servers and notebooks
56
+
57
+ Example:
58
+
59
+ ```python
60
+ from kystdatahuset.auth import login
61
+ import os
62
+
63
+ login_response = login("username", "password")
64
+ jwt = auth_res.data.JWT
65
+ voyages = get_voyages_for_ships_by_mmsi(
66
+ auth_jwt=jwt,
67
+ mmsi_ids=[258090000, 259028000],
68
+ start_date=datetime(2024,1,1),
69
+ end_date=datetime(2024,5,1),
70
+ )
71
+
72
+ ```
73
+ ---
74
+
75
+ ## 🌍 Efficient & “Social” Data Access
76
+
77
+ Instead of fetching massive multi-GB extracts, the library is designed for **smart, cooperative usage patterns**, where users share infrastructure responsibly:
78
+
79
+ ### ✅ Time Window Batching
80
+ Fetch long time periods in small, safe slices, python/Pandas "periods"
81
+
82
+
83
+ ### ✅ Geographic Slicing
84
+ Request only the needed spatial extent by WKT filters
85
+
86
+ ---
87
+
88
+ ## 🧱 Library Structure
89
+
90
+ ```
91
+ +---kystdatahuset
92
+ | | ais.py
93
+ | | api_client.py
94
+ | | auth.py
95
+ | | const.py
96
+ | | file_storage.py
97
+ | | logging.py
98
+ | | voyage.py
99
+ | | __init__.py
100
+ | |
101
+ | +---models
102
+ | | | AuthData.py
103
+ | | | FileListing.py
104
+ | | | WebServiceResponse.py
105
+ | | | __init__.py
106
+ | | |
107
+ | |
108
+ | +---types
109
+ | | | PandasFrequency.py
110
+ | | | UploadFileType.py
111
+ | | | __init__.py
112
+ | | |
113
+ | |
114
+ | +---utils
115
+ | | | _date_range.py
116
+ | | | __init__.py
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 📦 Development & Distribution
122
+
123
+ `kystdatahuset-py` uses standard packaging:
124
+
125
+ - `pyproject.toml` + `PEP 621` metadata
126
+ - versioning via Semantic Versioning
127
+ - full type hints (mypy-friendly)
128
+ - GitHub Actions for automated testing & publishing
129
+
130
+ ---
131
+
132
+ ## 🧠 Typical Use Cases
133
+
134
+ - Query live AIS vessel data efficiently
135
+ - Retrieve spatial datasets in bounded windows
136
+ - Build dashboards, decision-support tools, or AI/ML pipelines
137
+ - Avoid oversized extracts by using time/space batching helpers
138
+
139
+ ---
140
+
141
+ ## 📄 License
142
+
143
+ Open source under the **MIT License**.
144
+
@@ -0,0 +1,31 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/__init__.py
5
+ src/kystdatahuset/__init__.py
6
+ src/kystdatahuset/ais.py
7
+ src/kystdatahuset/api_client.py
8
+ src/kystdatahuset/auth.py
9
+ src/kystdatahuset/const.py
10
+ src/kystdatahuset/file_storage.py
11
+ src/kystdatahuset/logging.py
12
+ src/kystdatahuset/voyage.py
13
+ src/kystdatahuset/models/AuthData.py
14
+ src/kystdatahuset/models/FileListing.py
15
+ src/kystdatahuset/models/WebServiceResponse.py
16
+ src/kystdatahuset/models/__init__.py
17
+ src/kystdatahuset/types/PandasFrequency.py
18
+ src/kystdatahuset/types/UploadFileType.py
19
+ src/kystdatahuset/types/__init__.py
20
+ src/kystdatahuset/utils/__init__.py
21
+ src/kystdatahuset/utils/_date_range.py
22
+ src/kystdatahuset/utils/_slice_polygon_to_grid.py
23
+ src/kystdatahuset_python_lib.egg-info/PKG-INFO
24
+ src/kystdatahuset_python_lib.egg-info/SOURCES.txt
25
+ src/kystdatahuset_python_lib.egg-info/dependency_links.txt
26
+ src/kystdatahuset_python_lib.egg-info/requires.txt
27
+ src/kystdatahuset_python_lib.egg-info/top_level.txt
28
+ tests/test_ais.py
29
+ tests/test_auth.py
30
+ tests/test_file_storage.py
31
+ tests/test_voyage.py
@@ -0,0 +1,6 @@
1
+ numpy>=1.24
2
+ pandas>=2.0
3
+ pydantic>=2.0
4
+ requests>=2.28
5
+ shapely>=2.0
6
+ tqdm>=4.65
@@ -0,0 +1,18 @@
1
+ from datetime import datetime
2
+ from conftest import auth_jwt, wkt
3
+ from kystdatahuset.ais import get_ais_positions_within_geom_time
4
+ from kystdatahuset.logging import logger
5
+
6
+ def test_get_ais_pos_for_geom_time(auth_jwt, wkt):
7
+ response = get_ais_positions_within_geom_time(
8
+ jwt_token=auth_jwt,
9
+ geometry=wkt,
10
+ start_time=datetime(2024, 1, 1),
11
+ end_time=datetime(2024, 1, 2),
12
+ )
13
+ assert response is not None, "Expected a response, got None"
14
+ assert isinstance(response, list), "Expected response to be a list"
15
+ assert len(response) > 0, "Expected non-empty response list"
16
+ assert all(len(item) == 12 for item in response), "Expected all items in response to be of length >= 5"
17
+ logger.info(f"Response sample: {response[:2]}")
18
+ logger.info(f"Retrieved {len(response)} AIS positions")
@@ -0,0 +1,8 @@
1
+ from kystdatahuset.auth import login
2
+ import os
3
+
4
+ def test_login_success():
5
+ response = login(os.getenv("TEST_USERNAME"), os.getenv("TEST_PASSWORD"))
6
+ assert response is not None, "Expected a response, got None"
7
+ assert response.data is not None, "Response does not contain 'data'"
8
+ assert response.data.JWT is not None, "Response 'data' does not contain 'JWT'"
@@ -0,0 +1,24 @@
1
+ from kystdatahuset.file_storage import publish, list, delete
2
+ from uuid import UUID
3
+ from typing import List
4
+ from glob import glob
5
+ from pathlib import Path
6
+
7
+ def test_publish(upload_filename, auth_jwt):
8
+ publish_res = publish(jwt_token=auth_jwt, resource_uuid=UUID("64127fc2-2644-4ed9-b886-fecfb914c4b5"), file_path=upload_filename, title="Testfile", upload_file_type="csv", description="This is a test upload", categories=["test", "upload"], compressed=True)
9
+ assert publish_res is not None, "Expected publish to return True"
10
+
11
+ def test_publish_mfiles(auth_jwt):
12
+ files = glob(r"C:\Users\runar.bergheim\Documents\Development\nsr2csv\tmp\*.zip")
13
+ for file in files:
14
+ publish_res = publish(jwt_token=auth_jwt, resource_uuid=UUID("8e16b69e-d466-47bd-91d2-179aebdf4f65"), file_path=file, title=Path(file).name, upload_file_type="csv", description="Månedsfil med meldepliktige seilas fra SafeSeaNet", categories=["test", "upload"], compressed=True)
15
+ assert publish_res is not None, "Expected publish to return True"
16
+
17
+ def test_list(auth_jwt):
18
+ list_res = list(jwt_token=auth_jwt, resource_uuid=UUID("64127fc2-2644-4ed9-b886-fecfb914c4b5"))
19
+ assert isinstance(list_res, List) and len(list_res) >= 0, "Expected non-empty file list"
20
+
21
+ def test_delete(auth_jwt):
22
+ list_res = list(jwt_token=auth_jwt, resource_uuid=UUID("64127fc2-2644-4ed9-b886-fecfb914c4b5"))
23
+ delete_res = delete(jwt_token=auth_jwt, file_uuid=list_res[0].uuid)
24
+ assert delete_res == True, "Expected delete to return True"
@@ -0,0 +1,16 @@
1
+ from kystdatahuset.voyage import get_voyages_for_ships_by_mmsi
2
+ from kystdatahuset.logging import logger
3
+ from datetime import datetime
4
+ from conftest import auth_jwt
5
+
6
+ def test_get_voyages_for_ships_by_mmsi(auth_jwt):
7
+ response = get_voyages_for_ships_by_mmsi(
8
+ auth_jwt=auth_jwt,
9
+ mmsi_ids=[258090000, 259028000],
10
+ start_date=datetime(2024,1,1),
11
+ end_date=datetime(2024,5,1),
12
+ freq="MS"
13
+ )
14
+ assert response is not None, "Expected a response, got None"
15
+ assert isinstance(response, list), "Expected response to be a list"
16
+ logger.info(f"Retrieved {len(response)} voyages")