fcp-python 0.1.0__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.
fcp_python/main.py ADDED
@@ -0,0 +1,288 @@
1
+ """fcp-python — Python Code Intelligence FCP MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from fastmcp import FastMCP
11
+
12
+ from fcp_core import VerbRegistry, suggest
13
+
14
+ from fcp_python.domain.format import format_error
15
+ from fcp_python.domain.model import PythonModel
16
+ from fcp_python.domain.mutation import dispatch_mutation
17
+ from fcp_python.domain.query import dispatch_query
18
+ from fcp_python.domain.verbs import (
19
+ register_mutation_verbs,
20
+ register_query_verbs,
21
+ register_session_verbs,
22
+ )
23
+ from fcp_python.lsp.client import LspClient
24
+ from fcp_python.lsp.lifecycle import ServerStatus
25
+ from fcp_python.lsp.types import PublishDiagnosticsParams, SymbolInformation
26
+ from fcp_python.resolver.index import SymbolEntry, SymbolIndex
27
+
28
+ mcp = FastMCP(
29
+ "python-fcp",
30
+ instructions=(
31
+ "FCP Python server for querying Python codebases via pylsp. "
32
+ "Use python_session to open a workspace, python_query for read-only queries, "
33
+ "and python_help for the reference card."
34
+ ),
35
+ )
36
+
37
+ # Global state
38
+ _model = PythonModel("file:///")
39
+ _lock = asyncio.Lock()
40
+
41
+
42
+ def _make_registry() -> VerbRegistry:
43
+ reg = VerbRegistry()
44
+ register_query_verbs(reg)
45
+ register_mutation_verbs(reg)
46
+ register_session_verbs(reg)
47
+ return reg
48
+
49
+
50
+ _registry = _make_registry()
51
+
52
+
53
+ @mcp.tool(
54
+ description=(
55
+ "Execute Python mutation operations. Examples: "
56
+ "'rename Config Settings', "
57
+ "'extract validate @file:server.py @lines:15-30', "
58
+ "'import os @file:main.py @line:5'"
59
+ )
60
+ )
61
+ async def python(ops: list[str]) -> str:
62
+ """Execute mutation operations."""
63
+ async with _lock:
64
+ results = []
65
+ for op in ops:
66
+ result = await dispatch_mutation(_model, _registry, op)
67
+ results.append(result)
68
+ return "\n\n".join(results)
69
+
70
+
71
+ @mcp.tool(
72
+ description=(
73
+ "Execute a read-only FCP query on the Python workspace. Examples: "
74
+ "'find Config', 'def main @file:main.py', 'diagnose', 'unused', 'map'"
75
+ )
76
+ )
77
+ async def python_query(input: str) -> str:
78
+ """Execute a read-only query."""
79
+ async with _lock:
80
+ return await dispatch_query(_model, _registry, input)
81
+
82
+
83
+ @mcp.tool(
84
+ description=(
85
+ "Manage the Python workspace session. Actions: "
86
+ "'open PATH' to open a workspace, "
87
+ "'status' to check server status, "
88
+ "'close' to close the workspace."
89
+ )
90
+ )
91
+ async def python_session(action: str) -> str:
92
+ """Manage workspace session."""
93
+ async with _lock:
94
+ return await _handle_session(action)
95
+
96
+
97
+ @mcp.tool(
98
+ description="Show the FCP Python reference card with all available verbs and their syntax."
99
+ )
100
+ async def python_help() -> str:
101
+ """Show reference card."""
102
+ extra = {
103
+ "Selectors": (
104
+ " @file:PATH — filter by file path\n"
105
+ " @class:NAME — filter by containing class\n"
106
+ " @kind:KIND — filter by symbol kind (function, class, method, variable, ...)\n"
107
+ " @module:NAME — filter by module\n"
108
+ " @line:N — filter by line number\n"
109
+ " @lines:N-M — line range for extract\n"
110
+ " @decorator:NAME — filter by decorator"
111
+ ),
112
+ "Mutation Examples": (
113
+ ' python ["rename Config Settings"] — cross-file semantic rename\n'
114
+ ' python ["extract validate @file:server.py @lines:15-30"] — extract function\n'
115
+ ' python ["import os @file:main.py @line:5"] — add missing import'
116
+ ),
117
+ }
118
+ return _registry.generate_reference_card(extra)
119
+
120
+
121
+ async def _handle_session(action: str) -> str:
122
+ global _model
123
+ tokens = action.split()
124
+ if not tokens:
125
+ return "! empty session action."
126
+
127
+ cmd = tokens[0]
128
+ if cmd == "open":
129
+ if len(tokens) < 2:
130
+ return "! open requires a path."
131
+ return await _handle_open(tokens[1])
132
+ elif cmd == "status":
133
+ return _handle_status()
134
+ elif cmd == "close":
135
+ return await _handle_close()
136
+ else:
137
+ return f"! unknown session action '{cmd}'."
138
+
139
+
140
+ async def _handle_open(path: str) -> str:
141
+ global _model
142
+
143
+ if path.startswith("file://"):
144
+ uri = path
145
+ else:
146
+ p = Path(path).resolve()
147
+ if not p.exists():
148
+ return f"! path not found: {path}"
149
+ uri = p.as_uri()
150
+
151
+ try:
152
+ client = await LspClient.spawn("pylsp", [], uri)
153
+ except Exception as e:
154
+ return f"! failed to start pylsp: {e}"
155
+
156
+ _model = PythonModel(uri)
157
+ _model.lsp_client = client
158
+ _model.server_status = ServerStatus.Ready
159
+ _model.py_file_count = _count_py_files(path)
160
+
161
+ # Start notification handler
162
+ asyncio.create_task(_notification_handler(client.notification_queue, _model))
163
+
164
+ # Populate initial index
165
+ symbol_count = await _populate_initial_index(client, _model)
166
+
167
+ _model.last_reload = time.time()
168
+
169
+ return f"Opened workspace: {path} ({_model.py_file_count} files, {symbol_count} symbols)"
170
+
171
+
172
+ def _handle_status() -> str:
173
+ status_str = _model.server_status.name
174
+ errors, warnings = _model.total_diagnostics()
175
+ return (
176
+ f"Status: {status_str}\n"
177
+ f"Workspace: {_model.root_uri}\n"
178
+ f"Files: {_model.py_file_count}\n"
179
+ f"Symbols: {_model.symbol_index.size()}\n"
180
+ f"Diagnostics: {errors} errors, {warnings} warnings"
181
+ )
182
+
183
+
184
+ async def _handle_close() -> str:
185
+ global _model
186
+ if _model.lsp_client:
187
+ try:
188
+ await _model.lsp_client.shutdown()
189
+ except Exception:
190
+ pass
191
+
192
+ _model.server_status = ServerStatus.Stopped
193
+ _model.lsp_client = None
194
+ _model.symbol_index = SymbolIndex()
195
+ _model.diagnostics.clear()
196
+ _model.open_documents.clear()
197
+
198
+ return "Workspace closed."
199
+
200
+
201
+ async def _notification_handler(
202
+ queue: asyncio.Queue,
203
+ model: PythonModel,
204
+ ) -> None:
205
+ """Process LSP notifications (diagnostics etc)."""
206
+ while True:
207
+ try:
208
+ notif = await queue.get()
209
+ except Exception:
210
+ break
211
+ if notif.method == "textDocument/publishDiagnostics":
212
+ if notif.params:
213
+ try:
214
+ params = PublishDiagnosticsParams.from_dict(notif.params)
215
+ model.update_diagnostics(params.uri, params.diagnostics)
216
+ except Exception:
217
+ pass
218
+
219
+
220
+ async def _populate_initial_index(client: LspClient, model: PythonModel) -> int:
221
+ """Populate symbol index with workspace/symbol query, with retries."""
222
+ for attempt in range(10):
223
+ try:
224
+ raw_symbols = await client.request("workspace/symbol", {"query": "*"})
225
+ if raw_symbols:
226
+ for sym_dict in raw_symbols:
227
+ sym = SymbolInformation.from_dict(sym_dict)
228
+ model.symbol_index.insert(SymbolEntry(
229
+ name=sym.name,
230
+ kind=sym.kind,
231
+ container_name=sym.container_name,
232
+ uri=sym.location.uri,
233
+ range=sym.location.range,
234
+ selection_range=sym.location.range,
235
+ ))
236
+ return len(raw_symbols)
237
+ except Exception:
238
+ pass
239
+ if attempt < 9:
240
+ await asyncio.sleep(0.5)
241
+ return 0
242
+
243
+
244
+ def _count_py_files(path: str) -> int:
245
+ """Count .py files in directory, skipping hidden dirs and __pycache__."""
246
+ if not os.path.isdir(path):
247
+ return 0
248
+ skip_dirs = {"__pycache__", "node_modules", ".venv", "venv"}
249
+ count = 0
250
+ try:
251
+ for entry in os.scandir(path):
252
+ if entry.is_dir(follow_symlinks=False):
253
+ if entry.name.startswith(".") or entry.name in skip_dirs:
254
+ continue
255
+ count += _count_py_files(entry.path)
256
+ elif entry.name.endswith(".py"):
257
+ count += 1
258
+ except PermissionError:
259
+ pass
260
+ return count
261
+
262
+
263
+ def main() -> None:
264
+ # Spawn slipstream bridge in background (silent no-op if daemon not running)
265
+ from fcp_python.bridge import start_bridge
266
+
267
+ async def _bridge_session(action: str) -> str:
268
+ async with _lock:
269
+ return await _handle_session(action)
270
+
271
+ async def _bridge_query(q: str) -> str:
272
+ async with _lock:
273
+ return await dispatch_query(_model, _registry, q)
274
+
275
+ async def _bridge_mutation(ops: list[str]) -> str:
276
+ async with _lock:
277
+ results = []
278
+ for op in ops:
279
+ results.append(await dispatch_mutation(_model, _registry, op))
280
+ return "\n\n".join(results)
281
+
282
+ start_bridge(_bridge_session, _bridge_query, _bridge_mutation)
283
+
284
+ mcp.run()
285
+
286
+
287
+ if __name__ == "__main__":
288
+ main()
@@ -0,0 +1,25 @@
1
+ """Resolver layer: symbol indexing, selector filtering, resolution pipeline."""
2
+
3
+ from fcp_python.resolver.index import SymbolEntry, SymbolIndex
4
+ from fcp_python.resolver.selectors import (
5
+ ParsedSelector,
6
+ SelectorType,
7
+ filter_by_selectors,
8
+ parse_line_range,
9
+ parse_selector,
10
+ symbol_kind_from_string,
11
+ )
12
+ from fcp_python.resolver.pipeline import ResolveResult, SymbolResolver
13
+
14
+ __all__ = [
15
+ "SymbolEntry",
16
+ "SymbolIndex",
17
+ "ParsedSelector",
18
+ "SelectorType",
19
+ "filter_by_selectors",
20
+ "parse_line_range",
21
+ "parse_selector",
22
+ "symbol_kind_from_string",
23
+ "ResolveResult",
24
+ "SymbolResolver",
25
+ ]
@@ -0,0 +1,55 @@
1
+ """Triple-indexed symbol cache for fast resolution lookups."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from fcp_python.lsp.types import Range, SymbolKind
8
+
9
+
10
+ @dataclass
11
+ class SymbolEntry:
12
+ name: str
13
+ kind: SymbolKind
14
+ container_name: str | None
15
+ uri: str
16
+ range: Range
17
+ selection_range: Range
18
+
19
+
20
+ class SymbolIndex:
21
+ """Triple-indexed symbol store: by name, by file URI, by container."""
22
+
23
+ def __init__(self) -> None:
24
+ self._by_name: dict[str, list[SymbolEntry]] = {}
25
+ self._by_file: dict[str, list[SymbolEntry]] = {}
26
+ self._by_container: dict[str, list[SymbolEntry]] = {}
27
+
28
+ def insert(self, entry: SymbolEntry) -> None:
29
+ self._by_name.setdefault(entry.name, []).append(entry)
30
+ self._by_file.setdefault(entry.uri, []).append(entry)
31
+ if entry.container_name is not None:
32
+ self._by_container.setdefault(entry.container_name, []).append(entry)
33
+
34
+ def lookup_by_name(self, name: str) -> list[SymbolEntry]:
35
+ return list(self._by_name.get(name, []))
36
+
37
+ def lookup_by_file(self, uri: str) -> list[SymbolEntry]:
38
+ return list(self._by_file.get(uri, []))
39
+
40
+ def lookup_by_container(self, container: str) -> list[SymbolEntry]:
41
+ return list(self._by_container.get(container, []))
42
+
43
+ def invalidate_file(self, uri: str) -> None:
44
+ self._by_file.pop(uri, None)
45
+
46
+ for entries in self._by_name.values():
47
+ entries[:] = [e for e in entries if e.uri != uri]
48
+ self._by_name = {k: v for k, v in self._by_name.items() if v}
49
+
50
+ for entries in self._by_container.values():
51
+ entries[:] = [e for e in entries if e.uri != uri]
52
+ self._by_container = {k: v for k, v in self._by_container.items() if v}
53
+
54
+ def size(self) -> int:
55
+ return sum(len(v) for v in self._by_file.values())
@@ -0,0 +1,105 @@
1
+ """3-tier symbol resolution pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum, auto
7
+
8
+ from fcp_python.lsp.types import Location, SymbolInformation
9
+ from fcp_python.resolver.index import SymbolEntry, SymbolIndex
10
+ from fcp_python.resolver.selectors import ParsedSelector, filter_by_selectors
11
+
12
+
13
+ class _ResultKind(Enum):
14
+ FOUND = auto()
15
+ AMBIGUOUS = auto()
16
+ NOT_FOUND = auto()
17
+
18
+
19
+ class ResolveResult:
20
+ """Tagged union: Found(entry), Ambiguous(entries), NotFound."""
21
+
22
+ def __init__(self, kind: _ResultKind, entry: SymbolEntry | None = None, entries: list[SymbolEntry] | None = None):
23
+ self._kind = kind
24
+ self._entry = entry
25
+ self._entries = entries
26
+
27
+ @staticmethod
28
+ def found(entry: SymbolEntry) -> ResolveResult:
29
+ return ResolveResult(_ResultKind.FOUND, entry=entry)
30
+
31
+ @staticmethod
32
+ def ambiguous(entries: list[SymbolEntry]) -> ResolveResult:
33
+ return ResolveResult(_ResultKind.AMBIGUOUS, entries=entries)
34
+
35
+ @staticmethod
36
+ def not_found() -> ResolveResult:
37
+ return ResolveResult(_ResultKind.NOT_FOUND)
38
+
39
+ @property
40
+ def is_found(self) -> bool:
41
+ return self._kind == _ResultKind.FOUND
42
+
43
+ @property
44
+ def is_ambiguous(self) -> bool:
45
+ return self._kind == _ResultKind.AMBIGUOUS
46
+
47
+ @property
48
+ def is_not_found(self) -> bool:
49
+ return self._kind == _ResultKind.NOT_FOUND
50
+
51
+ @property
52
+ def entry(self) -> SymbolEntry:
53
+ assert self._kind == _ResultKind.FOUND and self._entry is not None
54
+ return self._entry
55
+
56
+ @property
57
+ def entries(self) -> list[SymbolEntry]:
58
+ assert self._kind == _ResultKind.AMBIGUOUS and self._entries is not None
59
+ return self._entries
60
+
61
+
62
+ def _entry_to_symbol_info(entry: SymbolEntry) -> SymbolInformation:
63
+ return SymbolInformation(
64
+ name=entry.name,
65
+ kind=entry.kind,
66
+ location=Location(uri=entry.uri, range=entry.range),
67
+ container_name=entry.container_name,
68
+ )
69
+
70
+
71
+ class SymbolResolver:
72
+ """Multi-tier symbol resolver."""
73
+
74
+ def __init__(self, index: SymbolIndex) -> None:
75
+ self._index = index
76
+
77
+ def resolve_from_index(
78
+ self,
79
+ name: str,
80
+ selectors: list[ParsedSelector],
81
+ ) -> ResolveResult:
82
+ """Tier 1: resolve from in-memory index with selector filtering."""
83
+ entries = self._index.lookup_by_name(name)
84
+
85
+ if not entries:
86
+ return ResolveResult.not_found()
87
+
88
+ if selectors:
89
+ sym_infos = [_entry_to_symbol_info(e) for e in entries]
90
+ filtered_infos = filter_by_selectors(sym_infos, selectors)
91
+ # Map back to entries by matching indices
92
+ filtered_info_set = set(id(si) for si in filtered_infos)
93
+ filtered = [
94
+ e for e, si in zip(entries, sym_infos)
95
+ if id(si) in filtered_info_set
96
+ ]
97
+ else:
98
+ filtered = entries
99
+
100
+ if len(filtered) == 0:
101
+ return ResolveResult.not_found()
102
+ elif len(filtered) == 1:
103
+ return ResolveResult.found(filtered[0])
104
+ else:
105
+ return ResolveResult.ambiguous(filtered)
@@ -0,0 +1,161 @@
1
+ """Selector parsing and filtering for symbol resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+ from fcp_python.lsp.types import SymbolInformation, SymbolKind
9
+
10
+
11
+ class SelectorType(Enum):
12
+ FILE = "file"
13
+ CLASS = "class"
14
+ KIND = "kind"
15
+ MODULE = "module"
16
+ LINE = "line"
17
+ LINES = "lines"
18
+ DECORATOR = "decorator"
19
+
20
+
21
+ @dataclass
22
+ class ParsedSelector:
23
+ selector_type: SelectorType
24
+ value: str
25
+
26
+
27
+ def parse_selector(selector: str) -> ParsedSelector | None:
28
+ """Parse @type:value selector string.
29
+
30
+ Maps: @file:PATH, @class:NAME (@struct:NAME alias),
31
+ @kind:KIND, @module:NAME (@mod:NAME alias),
32
+ @line:N, @lines:N-M, @decorator:NAME
33
+ """
34
+ if not selector.startswith("@"):
35
+ return None
36
+
37
+ rest = selector[1:]
38
+ colon_idx = rest.find(":")
39
+ if colon_idx == -1:
40
+ return None
41
+
42
+ type_str = rest[:colon_idx]
43
+ value = rest[colon_idx + 1:]
44
+
45
+ type_map: dict[str, SelectorType] = {
46
+ "file": SelectorType.FILE,
47
+ "class": SelectorType.CLASS,
48
+ "struct": SelectorType.CLASS, # Rust compat alias
49
+ "kind": SelectorType.KIND,
50
+ "module": SelectorType.MODULE,
51
+ "mod": SelectorType.MODULE,
52
+ "line": SelectorType.LINE,
53
+ "lines": SelectorType.LINES,
54
+ "decorator": SelectorType.DECORATOR,
55
+ }
56
+
57
+ selector_type = type_map.get(type_str)
58
+ if selector_type is None:
59
+ return None
60
+
61
+ return ParsedSelector(selector_type=selector_type, value=value)
62
+
63
+
64
+ def filter_by_selectors(
65
+ symbols: list[SymbolInformation],
66
+ selectors: list[ParsedSelector],
67
+ ) -> list[SymbolInformation]:
68
+ """Filter symbols by selectors (AND logic)."""
69
+ return [
70
+ sym for sym in symbols
71
+ if all(_matches_selector(sym, sel) for sel in selectors)
72
+ ]
73
+
74
+
75
+ def _matches_selector(sym: SymbolInformation, sel: ParsedSelector) -> bool:
76
+ match sel.selector_type:
77
+ case SelectorType.FILE:
78
+ return sel.value in sym.location.uri
79
+ case SelectorType.CLASS:
80
+ return (
81
+ sym.container_name == sel.value
82
+ or (sym.name == sel.value and sym.kind == SymbolKind.Class)
83
+ )
84
+ case SelectorType.KIND:
85
+ kind = symbol_kind_from_string(sel.value)
86
+ return kind is not None and sym.kind == kind
87
+ case SelectorType.MODULE:
88
+ container_match = (
89
+ sym.container_name is not None and sel.value in sym.container_name
90
+ )
91
+ return container_match or sel.value in sym.location.uri
92
+ case SelectorType.LINE:
93
+ try:
94
+ line = int(sel.value)
95
+ except ValueError:
96
+ return False
97
+ return sym.location.range.start.line <= line <= sym.location.range.end.line
98
+ case SelectorType.LINES:
99
+ # Consumed by mutation handlers, not symbol filtering
100
+ return True
101
+ case SelectorType.DECORATOR:
102
+ # Would need AST analysis; pass-through for now
103
+ return True
104
+
105
+
106
+ def parse_line_range(value: str) -> tuple[int, int] | None:
107
+ """Parse '15-30' into (15, 30). Returns None if invalid."""
108
+ parts = value.split("-", 1)
109
+ if len(parts) != 2:
110
+ return None
111
+ try:
112
+ start = int(parts[0])
113
+ end = int(parts[1])
114
+ except ValueError:
115
+ return None
116
+ if start > end:
117
+ return None
118
+ return (start, end)
119
+
120
+
121
+ def symbol_kind_from_string(s: str) -> SymbolKind | None:
122
+ """Convert string to SymbolKind. Python-specific additions included."""
123
+ mapping: dict[str, SymbolKind] = {
124
+ "function": SymbolKind.Function,
125
+ "fn": SymbolKind.Function,
126
+ "def": SymbolKind.Function,
127
+ "method": SymbolKind.Method,
128
+ "class": SymbolKind.Class,
129
+ "struct": SymbolKind.Struct,
130
+ "enum": SymbolKind.Enum,
131
+ "interface": SymbolKind.Interface,
132
+ "trait": SymbolKind.Interface,
133
+ "variable": SymbolKind.Variable,
134
+ "var": SymbolKind.Variable,
135
+ "constant": SymbolKind.Constant,
136
+ "const": SymbolKind.Constant,
137
+ "property": SymbolKind.Property,
138
+ "module": SymbolKind.Module,
139
+ "mod": SymbolKind.Module,
140
+ "namespace": SymbolKind.Namespace,
141
+ "field": SymbolKind.Field,
142
+ "constructor": SymbolKind.Constructor,
143
+ "type_parameter": SymbolKind.TypeParameter,
144
+ "typeparameter": SymbolKind.TypeParameter,
145
+ "file": SymbolKind.File,
146
+ "package": SymbolKind.Package,
147
+ "string": SymbolKind.String,
148
+ "number": SymbolKind.Number,
149
+ "boolean": SymbolKind.Boolean,
150
+ "bool": SymbolKind.Boolean,
151
+ "array": SymbolKind.Array,
152
+ "object": SymbolKind.Object,
153
+ "key": SymbolKind.Key,
154
+ "null": SymbolKind.Null,
155
+ "enum_member": SymbolKind.EnumMember,
156
+ "enummember": SymbolKind.EnumMember,
157
+ "event": SymbolKind.Event,
158
+ "operator": SymbolKind.Operator,
159
+ "decorator": SymbolKind.Function, # decorators are functions
160
+ }
161
+ return mapping.get(s.lower())
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: fcp-python
3
+ Version: 0.1.0
4
+ Summary: Python Code Intelligence FCP — semantic Python code operations for LLMs via pylsp
5
+ Requires-Python: <3.14,>=3.11
6
+ Requires-Dist: fastmcp>=3.0
7
+ Requires-Dist: fcp-core>=0.1.17
8
+ Requires-Dist: python-lsp-server[rope]>=1.12
@@ -0,0 +1,23 @@
1
+ fcp_python/__init__.py,sha256=jXbt2O90IFhcdC8JQ4qr2zYFSlz1Zvpml9hEam5JstU,58
2
+ fcp_python/bridge.py,sha256=KLfOkCdZR-KJsKzvnsus7PgLCbjrWK3NmTYFV-0hdEU,5680
3
+ fcp_python/main.py,sha256=nc_tjq8GfXXBJavLQP7ey6s_ROs7fOUyCYtnaSkx_6Q,8787
4
+ fcp_python/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ fcp_python/domain/format.py,sha256=9oHtn3d9vvRT28DrJxNs9m-YpIWjfjIr-pMWF5Tu4v0,7311
6
+ fcp_python/domain/model.py,sha256=yI2kVvEWv50wICqqmrHV_302EVMtZd_t1v1X50ZonO8,1458
7
+ fcp_python/domain/mutation.py,sha256=ggkm-4zY9rTHfNYYWEcfGNok14MFXUS6_nsBVXaQqoU,12578
8
+ fcp_python/domain/query.py,sha256=KFve1HFAKwyHJPgT1INEQ97jieUUDvhswho-NHqrxkc,21706
9
+ fcp_python/domain/verbs.py,sha256=3u2SJ5do1UKXSA39KaCDvdTkhMXHpUAhm363kEQKDXs,1919
10
+ fcp_python/lsp/__init__.py,sha256=Kai6_M5ap1qjrLEfpkdcVZenIzQDQHMUu28JscQg_kM,39
11
+ fcp_python/lsp/client.py,sha256=H-I1Hq8ayAUimGcsMpiLmFZcdYHRvQBL0jYG--F51JU,6428
12
+ fcp_python/lsp/lifecycle.py,sha256=i-b61LGOeQxZIUjUAf_oR8nMqS7rnGR52EYb9KM3zHc,2774
13
+ fcp_python/lsp/transport.py,sha256=XN3vOMa3aT_uBh_-s05mxxwf4w_YF9n3a-D7tJS578k,3616
14
+ fcp_python/lsp/types.py,sha256=aj_5U-dHX-pXMjW6UerYGdQV8lwT8kHn58U26R0Cgm4,13332
15
+ fcp_python/lsp/workspace_edit.py,sha256=WDT1bsPW38fI2k4_SaMpHONBds96oTKBlIPiVhVDixA,4204
16
+ fcp_python/resolver/__init__.py,sha256=9ImXXfUvA0TWGDZx1GMniKzav7SKT9q9u1C_0EOZE9w,637
17
+ fcp_python/resolver/index.py,sha256=9y56ZoBNqwl8a4_Ue54ZBjNWzj3kZ548miIZvab_90k,1859
18
+ fcp_python/resolver/pipeline.py,sha256=cPqawRgQ3hxNKottcsco1RGLT4f2c3IWidqnuSzhh7c,3174
19
+ fcp_python/resolver/selectors.py,sha256=u_NQdp6eU2fPSKnSpWNhTZbxwy6OgvxVTR3pKdB_7Rg,5070
20
+ fcp_python-0.1.0.dist-info/METADATA,sha256=mwRqNS2f4dkcGWab_RdZ2PBcgB0Y85MK6n1yMOf2sUM,282
21
+ fcp_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
22
+ fcp_python-0.1.0.dist-info/entry_points.txt,sha256=iSyoGO8uLz21KTX8mL3twnRXWsuc7-8O3U03WCSiJ_s,52
23
+ fcp_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fcp-python = fcp_python.main:main