koi-net 1.2.0b1__py3-none-any.whl → 1.2.0b3__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.

Potentially problematic release.


This version of koi-net might be problematic. Click here for more details.

Files changed (52) hide show
  1. koi_net/__init__.py +1 -1
  2. koi_net/assembler.py +95 -0
  3. koi_net/cli/models.py +2 -2
  4. koi_net/config/core.py +71 -0
  5. koi_net/config/full_node.py +31 -0
  6. koi_net/config/loader.py +46 -0
  7. koi_net/config/partial_node.py +18 -0
  8. koi_net/core.py +43 -206
  9. koi_net/default_actions.py +1 -2
  10. koi_net/effector.py +27 -15
  11. koi_net/entrypoints/__init__.py +2 -0
  12. koi_net/entrypoints/base.py +5 -0
  13. koi_net/{poller.py → entrypoints/poller.py} +14 -12
  14. koi_net/entrypoints/server.py +94 -0
  15. koi_net/handshaker.py +5 -5
  16. koi_net/identity.py +3 -4
  17. koi_net/lifecycle.py +42 -34
  18. koi_net/logger.py +176 -0
  19. koi_net/network/error_handler.py +7 -7
  20. koi_net/network/event_queue.py +9 -7
  21. koi_net/network/graph.py +8 -8
  22. koi_net/network/poll_event_buffer.py +26 -0
  23. koi_net/network/request_handler.py +23 -28
  24. koi_net/network/resolver.py +14 -14
  25. koi_net/network/response_handler.py +74 -9
  26. koi_net/{context.py → processor/context.py} +11 -19
  27. koi_net/processor/handler.py +4 -1
  28. koi_net/processor/{default_handlers.py → knowledge_handlers.py} +32 -31
  29. koi_net/processor/knowledge_object.py +2 -3
  30. koi_net/processor/kobj_queue.py +4 -4
  31. koi_net/processor/{knowledge_pipeline.py → pipeline.py} +25 -28
  32. koi_net/protocol/api_models.py +5 -2
  33. koi_net/protocol/envelope.py +5 -6
  34. koi_net/protocol/model_map.py +61 -0
  35. koi_net/protocol/node.py +3 -3
  36. koi_net/protocol/secure.py +14 -8
  37. koi_net/secure.py +6 -7
  38. koi_net/workers/__init__.py +2 -0
  39. koi_net/{worker.py → workers/base.py} +7 -0
  40. koi_net/{processor → workers}/event_worker.py +19 -23
  41. koi_net/{kobj_worker.py → workers/kobj_worker.py} +12 -13
  42. {koi_net-1.2.0b1.dist-info → koi_net-1.2.0b3.dist-info}/METADATA +2 -1
  43. koi_net-1.2.0b3.dist-info/RECORD +56 -0
  44. koi_net/behaviors.py +0 -51
  45. koi_net/config.py +0 -161
  46. koi_net/models.py +0 -14
  47. koi_net/poll_event_buffer.py +0 -17
  48. koi_net/server.py +0 -145
  49. koi_net-1.2.0b1.dist-info/RECORD +0 -49
  50. {koi_net-1.2.0b1.dist-info → koi_net-1.2.0b3.dist-info}/WHEEL +0 -0
  51. {koi_net-1.2.0b1.dist-info → koi_net-1.2.0b3.dist-info}/entry_points.txt +0 -0
  52. {koi_net-1.2.0b1.dist-info → koi_net-1.2.0b3.dist-info}/licenses/LICENSE +0 -0
koi_net/behaviors.py DELETED
@@ -1,51 +0,0 @@
1
- from logging import getLogger
2
- from rid_lib.ext import Cache
3
- from rid_lib.types import KoiNetNode
4
- from rid_lib import RIDType
5
- from koi_net.identity import NodeIdentity
6
- from koi_net.network.event_queue import EventQueue
7
- from koi_net.network.request_handler import RequestHandler
8
- from koi_net.network.resolver import NetworkResolver
9
- from koi_net.processor.kobj_queue import KobjQueue
10
- from koi_net.protocol.api_models import ErrorResponse
11
- from .protocol.event import Event, EventType
12
-
13
-
14
- logger = getLogger(__name__)
15
-
16
-
17
-
18
- class Behaviors:
19
- def __init__(self, cache: Cache, identity: NodeIdentity, event_queue: EventQueue, resolver: NetworkResolver, request_handler: RequestHandler, kobj_queue: KobjQueue):
20
- self.cache = cache
21
- self.identity = identity
22
- self.event_queue = event_queue
23
- self.resolver = resolver
24
- self.request_handler = request_handler
25
- self.kobj_queue = kobj_queue
26
-
27
- def identify_coordinators(self) -> list[KoiNetNode]:
28
- """Returns node's providing state for `orn:koi-net.node`."""
29
- return self.resolver.get_state_providers(KoiNetNode)
30
-
31
- def catch_up_with(self, target: KoiNetNode, rid_types: list[RIDType] = []):
32
- """Fetches and processes knowledge objects from target node.
33
- Args:
34
- target: Node to catch up with
35
- rid_types: RID types to fetch from target (all types if list is empty)
36
- """
37
- logger.debug(f"catching up with {target} on {rid_types or 'all types'}")
38
- payload = self.request_handler.fetch_manifests(
39
- node=target,
40
- rid_types=rid_types
41
- )
42
- if type(payload) == ErrorResponse:
43
- logger.debug("failed to reach node")
44
- return
45
- for manifest in payload.manifests:
46
- if manifest.rid == self.identity.rid:
47
- continue
48
- self.kobj_queue.put_kobj(
49
- manifest=manifest,
50
- source=target
51
- )
koi_net/config.py DELETED
@@ -1,161 +0,0 @@
1
- import os
2
- from rid_lib import RIDType
3
- from ruamel.yaml import YAML
4
- from pydantic import BaseModel, Field, PrivateAttr
5
- from dotenv import load_dotenv
6
- from rid_lib.ext.utils import sha256_hash
7
- from rid_lib.types import KoiNetNode
8
- from .protocol.secure import PrivateKey
9
- from .protocol.node import NodeProfile, NodeType
10
-
11
-
12
- class ServerConfig(BaseModel):
13
- """Config for the node server (full node only)."""
14
-
15
- host: str = "127.0.0.1"
16
- port: int = 8000
17
- path: str | None = "/koi-net"
18
-
19
- @property
20
- def url(self) -> str:
21
- return f"http://{self.host}:{self.port}{self.path or ''}"
22
-
23
- class NodeContact(BaseModel):
24
- rid: KoiNetNode | None = None
25
- url: str | None = None
26
-
27
- class KoiNetConfig(BaseModel):
28
- """Config for KOI-net."""
29
-
30
- node_name: str
31
- node_rid: KoiNetNode | None = None
32
- node_profile: NodeProfile
33
-
34
- rid_types_of_interest: list[RIDType] = Field(
35
- default_factory=lambda: [KoiNetNode])
36
-
37
- cache_directory_path: str = ".rid_cache"
38
- event_queues_path: str = "event_queues.json"
39
- private_key_pem_path: str = "priv_key.pem"
40
- polling_interval: int = 5
41
-
42
- first_contact: NodeContact = Field(default_factory=NodeContact)
43
-
44
- _priv_key: PrivateKey | None = PrivateAttr(default=None)
45
-
46
- class EnvConfig(BaseModel):
47
- """Config for environment variables.
48
-
49
- Values set in the config are the variables names, and are loaded
50
- from the environment at runtime. For example, if the config YAML
51
- sets `priv_key_password: PRIV_KEY_PASSWORD` accessing
52
- `priv_key_password` would retrieve the value of `PRIV_KEY_PASSWORD`
53
- from the environment.
54
- """
55
-
56
- priv_key_password: str | None = "PRIV_KEY_PASSWORD"
57
-
58
- def __init__(self, **kwargs):
59
- super().__init__(**kwargs)
60
- load_dotenv()
61
-
62
- def __getattribute__(self, name):
63
- value = super().__getattribute__(name)
64
- if name in type(self).model_fields:
65
- env_val = os.getenv(value)
66
- if env_val is None:
67
- raise ValueError(f"Required environment variable {value} not set")
68
- return env_val
69
- return value
70
-
71
- class NodeConfig(BaseModel):
72
- """Base configuration class for all nodes.
73
-
74
- Designed to be extensible for custom node implementations. Classes
75
- inheriting from `NodeConfig` may add additional config groups.
76
- """
77
-
78
- server: ServerConfig = Field(default_factory=ServerConfig)
79
- koi_net: KoiNetConfig
80
- env: EnvConfig = Field(default_factory=EnvConfig)
81
-
82
- _file_path: str = PrivateAttr(default="config.yaml")
83
- _file_content: str | None = PrivateAttr(default=None)
84
-
85
- @classmethod
86
- def load_from_yaml(
87
- cls,
88
- file_path: str = "config.yaml",
89
- generate_missing: bool = True
90
- ):
91
- """Loads config state from YAML file.
92
-
93
- Defaults to `config.yaml`. If `generate_missing` is set to
94
- `True`, a private key and RID will be generated if not already
95
- present in the config.
96
- """
97
- yaml = YAML()
98
-
99
- try:
100
- with open(file_path, "r") as f:
101
- file_content = f.read()
102
- config_data = yaml.load(file_content)
103
- config = cls.model_validate(config_data)
104
- config._file_content = file_content
105
-
106
- except FileNotFoundError:
107
- # empty_fields = {}
108
- # for name, field in cls.model_fields.items():
109
-
110
- # if field.default is None or field.default_factory is None:
111
- # print(empty_fields)
112
- config = cls()
113
-
114
-
115
- config._file_path = file_path
116
-
117
- if generate_missing:
118
- if not config.koi_net.node_rid:
119
- priv_key = PrivateKey.generate()
120
- pub_key = priv_key.public_key()
121
-
122
- config.koi_net.node_rid = KoiNetNode(
123
- config.koi_net.node_name,
124
- sha256_hash(pub_key.to_der())
125
- )
126
-
127
- with open(config.koi_net.private_key_pem_path, "w") as f:
128
- f.write(
129
- priv_key.to_pem(config.env.priv_key_password)
130
- )
131
-
132
- config.koi_net.node_profile.public_key = pub_key.to_der()
133
-
134
- if config.koi_net.node_profile.node_type == NodeType.FULL:
135
- config.koi_net.node_profile.base_url = (
136
- config.koi_net.node_profile.base_url or config.server.url
137
- )
138
-
139
- config.save_to_yaml()
140
-
141
- return config
142
-
143
- def save_to_yaml(self):
144
- """Saves config state to YAML file.
145
-
146
- File path is set by `load_from_yaml` class method.
147
- """
148
-
149
- yaml = YAML()
150
-
151
- with open(self._file_path, "w") as f:
152
- try:
153
- config_data = self.model_dump(mode="json")
154
- yaml.dump(config_data, f)
155
- except Exception as e:
156
- if self._file_content:
157
- f.seek(0)
158
- f.truncate()
159
- f.write(self._file_content)
160
- raise e
161
-
koi_net/models.py DELETED
@@ -1,14 +0,0 @@
1
- from pydantic import BaseModel
2
- from rid_lib.types import KoiNetNode
3
- from koi_net.protocol.event import Event
4
-
5
- class End:
6
- """Class for a sentinel value by knowledge handlers."""
7
- pass
8
-
9
- END = End()
10
-
11
-
12
- class QueuedEvent(BaseModel):
13
- event: Event
14
- target: KoiNetNode
@@ -1,17 +0,0 @@
1
- from rid_lib.types import KoiNetNode
2
-
3
- from koi_net.protocol.event import Event
4
-
5
-
6
- class PollEventBuffer:
7
- buffers: dict[KoiNetNode, list[Event]]
8
-
9
- def __init__(self):
10
- self.buffers = dict()
11
-
12
- def put(self, node: KoiNetNode, event: Event):
13
- event_buf = self.buffers.setdefault(node, [])
14
- event_buf.append(event)
15
-
16
- def flush(self, node: KoiNetNode):
17
- return self.buffers.pop(node, [])
koi_net/server.py DELETED
@@ -1,145 +0,0 @@
1
- import logging
2
- import uvicorn
3
- from contextlib import asynccontextmanager
4
- from fastapi import FastAPI, APIRouter
5
- from fastapi.responses import JSONResponse
6
-
7
- from koi_net.poll_event_buffer import PollEventBuffer
8
- from .network.response_handler import ResponseHandler
9
- from .processor.kobj_queue import KobjQueue
10
- from .protocol.api_models import (
11
- PollEvents,
12
- FetchRids,
13
- FetchManifests,
14
- FetchBundles,
15
- EventsPayload,
16
- RidsPayload,
17
- ManifestsPayload,
18
- BundlesPayload,
19
- ErrorResponse
20
- )
21
- from .protocol.errors import ProtocolError
22
- from .protocol.envelope import SignedEnvelope
23
- from .protocol.consts import (
24
- BROADCAST_EVENTS_PATH,
25
- POLL_EVENTS_PATH,
26
- FETCH_RIDS_PATH,
27
- FETCH_MANIFESTS_PATH,
28
- FETCH_BUNDLES_PATH
29
- )
30
- from .secure import Secure
31
- from .lifecycle import NodeLifecycle
32
- from .config import NodeConfig
33
-
34
- logger = logging.getLogger(__name__)
35
-
36
-
37
- class NodeServer:
38
- """Manages FastAPI server and event handling for full nodes."""
39
- config: NodeConfig
40
- lifecycle: NodeLifecycle
41
- secure: Secure
42
- kobj_queue: KobjQueue
43
- poll_event_buf: PollEventBuffer
44
- response_handler: ResponseHandler
45
- app: FastAPI
46
- router: APIRouter
47
-
48
- def __init__(
49
- self,
50
- config: NodeConfig,
51
- lifecycle: NodeLifecycle,
52
- secure: Secure,
53
- kobj_queue: KobjQueue,
54
- poll_event_buf: PollEventBuffer,
55
- response_handler: ResponseHandler
56
- ):
57
- self.config = config
58
- self.lifecycle = lifecycle
59
- self.secure = secure
60
- self.kobj_queue = kobj_queue
61
- self.poll_event_buf = poll_event_buf
62
- self.response_handler = response_handler
63
- self._build_app()
64
-
65
- def _build_app(self):
66
- """Builds FastAPI app and adds endpoints."""
67
- @asynccontextmanager
68
- async def lifespan(*args, **kwargs):
69
- async with self.lifecycle.async_run():
70
- yield
71
-
72
- self.app = FastAPI(
73
- lifespan=lifespan,
74
- title="KOI-net Protocol API",
75
- version="1.0.0"
76
- )
77
-
78
- self.router = APIRouter(prefix="/koi-net")
79
- self.app.add_exception_handler(ProtocolError, self.protocol_error_handler)
80
-
81
- def _add_endpoint(path, func):
82
- self.router.add_api_route(
83
- path=path,
84
- endpoint=self.secure.envelope_handler(func),
85
- methods=["POST"],
86
- response_model_exclude_none=True
87
- )
88
-
89
- _add_endpoint(BROADCAST_EVENTS_PATH, self.broadcast_events)
90
- _add_endpoint(POLL_EVENTS_PATH, self.poll_events)
91
- _add_endpoint(FETCH_RIDS_PATH, self.fetch_rids)
92
- _add_endpoint(FETCH_MANIFESTS_PATH, self.fetch_manifests)
93
- _add_endpoint(FETCH_BUNDLES_PATH, self.fetch_bundles)
94
-
95
- self.app.include_router(self.router)
96
-
97
- def run(self):
98
- """Starts FastAPI server and event handler."""
99
- uvicorn.run(
100
- app=self.app,
101
- host=self.config.server.host,
102
- port=self.config.server.port
103
- )
104
-
105
- def protocol_error_handler(self, request, exc: ProtocolError):
106
- """Catches `ProtocolError` and returns as `ErrorResponse`."""
107
- logger.info(f"caught protocol error: {exc}")
108
- resp = ErrorResponse(error=exc.error_type)
109
- logger.info(f"returning error response: {resp}")
110
- return JSONResponse(
111
- status_code=400,
112
- content=resp.model_dump(mode="json")
113
- )
114
-
115
- async def broadcast_events(self, req: SignedEnvelope[EventsPayload]):
116
- """Handles events broadcast endpoint."""
117
- logger.info(f"Request to {BROADCAST_EVENTS_PATH}, received {len(req.payload.events)} event(s)")
118
- for event in req.payload.events:
119
- self.kobj_queue.put_kobj(event=event, source=req.source_node)
120
-
121
- async def poll_events(
122
- self, req: SignedEnvelope[PollEvents]
123
- ) -> SignedEnvelope[EventsPayload] | ErrorResponse:
124
- """Handles poll events endpoint."""
125
- logger.info(f"Request to {POLL_EVENTS_PATH}")
126
- events = self.poll_event_buf.flush(req.source_node)
127
- return EventsPayload(events=events)
128
-
129
- async def fetch_rids(
130
- self, req: SignedEnvelope[FetchRids]
131
- ) -> SignedEnvelope[RidsPayload] | ErrorResponse:
132
- """Handles fetch RIDs endpoint."""
133
- return self.response_handler.fetch_rids(req.payload, req.source_node)
134
-
135
- async def fetch_manifests(
136
- self, req: SignedEnvelope[FetchManifests]
137
- ) -> SignedEnvelope[ManifestsPayload] | ErrorResponse:
138
- """Handles fetch manifests endpoint."""
139
- return self.response_handler.fetch_manifests(req.payload, req.source_node)
140
-
141
- async def fetch_bundles(
142
- self, req: SignedEnvelope[FetchBundles]
143
- ) -> SignedEnvelope[BundlesPayload] | ErrorResponse:
144
- """Handles fetch bundles endpoint."""
145
- return self.response_handler.fetch_bundles(req.payload, req.source_node)
@@ -1,49 +0,0 @@
1
- koi_net/__init__.py,sha256=S6Ew8CDhwaysGFJGX19tgkXLcJiJR2R5fwz449gaXFY,31
2
- koi_net/behaviors.py,sha256=_2yeX3IdDQWiOsXoehiFjr2jPj58oZTnBX28M32LM4w,1955
3
- koi_net/config.py,sha256=yV53GFQrJchyKKRs3sOhj27pyM6rwiUhIgDZ81rvZ48,5238
4
- koi_net/context.py,sha256=z24-pwYE3YDIznqNkRBIZuqT-JpksHnkuUfDOH034j0,1429
5
- koi_net/core.py,sha256=qUq87KfL-DKUu0NVfJPFBiNYpD35ODzvP6bV7F5ijd4,7252
6
- koi_net/default_actions.py,sha256=y9oaDeb3jJGSvVbuonmyGWkKWlzpEX3V32MjgEuYWc8,622
7
- koi_net/effector.py,sha256=iFX34Tq9Byev22CUbGnQU1MUccyNH5ebmQzoovwY_aE,4738
8
- koi_net/handshaker.py,sha256=e5bzWGWvU7w5gQ4kBEiU6ShCXwc-XXgfvx3c1RxMguA,1238
9
- koi_net/identity.py,sha256=FvIWksGTqwM7HCevIwmo_6l-t-2tnYkaaR4CanZatL4,569
10
- koi_net/kobj_worker.py,sha256=p_1QJDDoz-cIDoTQ3qCptPTbuvqou5BAPEQqZjH8niE,1306
11
- koi_net/lifecycle.py,sha256=WP3XiiCtSG8ZGa4oBnNBxwsmQ35QRQEgeYqwnGRxcsI,4379
12
- koi_net/models.py,sha256=_hL-ohrlH-kZRV9B0PWjYyZbb_Ef1z14HOLI5RK_Dto,274
13
- koi_net/poll_event_buffer.py,sha256=DMuEnaoH7nZccXY0LwHaGEyecrWfW1z0s7AbVHMGM88,441
14
- koi_net/poller.py,sha256=zBHWPXvca-Ia9ggE5emu8rJx5c8Jdi1ZAYB7lTyezv8,1395
15
- koi_net/secure.py,sha256=1t1WtHeqmoHfi4siW1jFCmIhKFsQGactITkEmVYKqnc,4919
16
- koi_net/server.py,sha256=iP9agexIjqmNlCOXs50cLkyEWHth7cGlQRN593rBOhU,4955
17
- koi_net/utils.py,sha256=4K6BqtifExDUfPP8zJAICgPV1mcabz_MaFB0P0fSnic,411
18
- koi_net/worker.py,sha256=G3JLlILN0cyY7Fv3eA5y8uf9rImy4e21OQ1mEKFoVVo,191
19
- koi_net/cli/__init__.py,sha256=Vpq0yLN0ROyF0qQYsyiuWFbjs-zsa90G1DpAv13qaiw,25
20
- koi_net/cli/commands.py,sha256=5eVsvJAnHjwJdt8MNYrk5zSzWWm1ooiP35bJeQxYIq4,2592
21
- koi_net/cli/models.py,sha256=ON-rqMgHDng3J2hbBbGYzkynaWc9Tq4QfOJIu_6pFuA,1190
22
- koi_net/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- koi_net/network/error_handler.py,sha256=8XgOljFBJ4lzXdEO5I-spND9iRln9309lwgpPV1pMnM,1802
24
- koi_net/network/event_queue.py,sha256=rvTuMuG-QicKHGSfB2FOyFMngmnESovec1TOPIowvss,780
25
- koi_net/network/graph.py,sha256=8hweEivCwxnagFoUrefE7Hnz06MWScF3XJHeXU2Qqnw,4178
26
- koi_net/network/request_handler.py,sha256=1Sp0-OCqcQXM-fAE5sFH4_M9FP1EbJh69P8gB56BSKg,7102
27
- koi_net/network/resolver.py,sha256=MqUMwG5snZp4QLV7d-gZ7tRJDQ0uJZ_IhssozTc9JkU,5309
28
- koi_net/network/response_handler.py,sha256=tbTqHmwXUEZIqiYqVL2vi4q6eDH0bPRrAO6G4XpbMic,2123
29
- koi_net/processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
- koi_net/processor/default_handlers.py,sha256=siRZl_Ww7QA9MhJR4_nmrSHOG8ejfS0NjiF6braA338,11020
31
- koi_net/processor/event_worker.py,sha256=bS_Xgkg4s1e_5dG0qtHY3d79JsYEyGzbI6F7JSZbSws,4297
32
- koi_net/processor/handler.py,sha256=PJlQCPfKm3zYsJrwmGIw5xZmc6vArKz_UoeJuXBj16Y,2356
33
- koi_net/processor/knowledge_object.py,sha256=0VyBOqkCoINMK3fqYhLlXU-Ri7gKEpJloDHfhaGITxg,4172
34
- koi_net/processor/knowledge_pipeline.py,sha256=sz8T12nAcJ1tbF2jHf0Wmi6ffnQPMrIl64xif4WP7fc,8784
35
- koi_net/processor/kobj_queue.py,sha256=LIh9Sz3EaEZj9qJBd4iLPlzYBO6aH-6qt_JHCxAzAJU,1839
36
- koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- koi_net/protocol/api_models.py,sha256=AIFT49fyNAnOsHep9bgjga2M0S38-PouBaqfQI2zcKY,1795
38
- koi_net/protocol/consts.py,sha256=bisbVEojPIHlLhkLafBzfIhH25TjNfvTORF1g6YXzIM,243
39
- koi_net/protocol/edge.py,sha256=PzdEhC43T1KO5iMSEu7I4tiz-7sZxtz41dJfWf-oHA0,1034
40
- koi_net/protocol/envelope.py,sha256=UVHlO2BDyDiP5eixqx9xD6xUsCfFRi0kZyzC4BC-DOw,1886
41
- koi_net/protocol/errors.py,sha256=uKPQ-TGLouZuK0xd2pXuCQoRTyu_JFsydSCLml13Cz8,595
42
- koi_net/protocol/event.py,sha256=HxzLN-iCXPyr2YzrswMIkgZYeUdFbBpa5v98dAB06lQ,1328
43
- koi_net/protocol/node.py,sha256=KH_SjHDzW-V4pn2tqpTDhIzfuCgKgM1YqCafyofLN3k,466
44
- koi_net/protocol/secure.py,sha256=6sRLWxG5EDF0QLBj29gk3hPmZnPXATrTTFdwx39wQfY,5127
45
- koi_net-1.2.0b1.dist-info/METADATA,sha256=reMf5147lyI17Gu5Ca98sQFZsoMk9pL5mV2BHZKtpV0,37314
46
- koi_net-1.2.0b1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
- koi_net-1.2.0b1.dist-info/entry_points.txt,sha256=l7He_JTyXrfKIHkttnPWXHI717v8WpLLfCduL5QcEWA,40
48
- koi_net-1.2.0b1.dist-info/licenses/LICENSE,sha256=03mgCL5qth2aD9C3F3qNVs4sFJSpK9kjtYCyOwdSp7s,1069
49
- koi_net-1.2.0b1.dist-info/RECORD,,