affinity-sdk 0.9.5__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.
- affinity/__init__.py +139 -0
- affinity/cli/__init__.py +7 -0
- affinity/cli/click_compat.py +27 -0
- affinity/cli/commands/__init__.py +1 -0
- affinity/cli/commands/_entity_files_dump.py +219 -0
- affinity/cli/commands/_list_entry_fields.py +41 -0
- affinity/cli/commands/_v1_parsing.py +77 -0
- affinity/cli/commands/company_cmds.py +2139 -0
- affinity/cli/commands/completion_cmd.py +33 -0
- affinity/cli/commands/config_cmds.py +540 -0
- affinity/cli/commands/entry_cmds.py +33 -0
- affinity/cli/commands/field_cmds.py +413 -0
- affinity/cli/commands/interaction_cmds.py +875 -0
- affinity/cli/commands/list_cmds.py +3152 -0
- affinity/cli/commands/note_cmds.py +433 -0
- affinity/cli/commands/opportunity_cmds.py +1174 -0
- affinity/cli/commands/person_cmds.py +1980 -0
- affinity/cli/commands/query_cmd.py +444 -0
- affinity/cli/commands/relationship_strength_cmds.py +62 -0
- affinity/cli/commands/reminder_cmds.py +595 -0
- affinity/cli/commands/resolve_url_cmd.py +127 -0
- affinity/cli/commands/session_cmds.py +84 -0
- affinity/cli/commands/task_cmds.py +110 -0
- affinity/cli/commands/version_cmd.py +29 -0
- affinity/cli/commands/whoami_cmd.py +36 -0
- affinity/cli/config.py +108 -0
- affinity/cli/context.py +749 -0
- affinity/cli/csv_utils.py +195 -0
- affinity/cli/date_utils.py +42 -0
- affinity/cli/decorators.py +77 -0
- affinity/cli/errors.py +28 -0
- affinity/cli/field_utils.py +355 -0
- affinity/cli/formatters.py +551 -0
- affinity/cli/help_json.py +283 -0
- affinity/cli/logging.py +100 -0
- affinity/cli/main.py +261 -0
- affinity/cli/options.py +53 -0
- affinity/cli/paths.py +32 -0
- affinity/cli/progress.py +183 -0
- affinity/cli/query/__init__.py +163 -0
- affinity/cli/query/aggregates.py +357 -0
- affinity/cli/query/dates.py +194 -0
- affinity/cli/query/exceptions.py +147 -0
- affinity/cli/query/executor.py +1236 -0
- affinity/cli/query/filters.py +248 -0
- affinity/cli/query/models.py +333 -0
- affinity/cli/query/output.py +331 -0
- affinity/cli/query/parser.py +619 -0
- affinity/cli/query/planner.py +430 -0
- affinity/cli/query/progress.py +270 -0
- affinity/cli/query/schema.py +439 -0
- affinity/cli/render.py +1589 -0
- affinity/cli/resolve.py +222 -0
- affinity/cli/resolvers.py +249 -0
- affinity/cli/results.py +308 -0
- affinity/cli/runner.py +218 -0
- affinity/cli/serialization.py +65 -0
- affinity/cli/session_cache.py +276 -0
- affinity/cli/types.py +70 -0
- affinity/client.py +771 -0
- affinity/clients/__init__.py +19 -0
- affinity/clients/http.py +3664 -0
- affinity/clients/pipeline.py +165 -0
- affinity/compare.py +501 -0
- affinity/downloads.py +114 -0
- affinity/exceptions.py +615 -0
- affinity/filters.py +1128 -0
- affinity/hooks.py +198 -0
- affinity/inbound_webhooks.py +302 -0
- affinity/models/__init__.py +163 -0
- affinity/models/entities.py +798 -0
- affinity/models/pagination.py +513 -0
- affinity/models/rate_limit_snapshot.py +48 -0
- affinity/models/secondary.py +413 -0
- affinity/models/types.py +663 -0
- affinity/policies.py +40 -0
- affinity/progress.py +22 -0
- affinity/py.typed +0 -0
- affinity/services/__init__.py +42 -0
- affinity/services/companies.py +1286 -0
- affinity/services/lists.py +1892 -0
- affinity/services/opportunities.py +1330 -0
- affinity/services/persons.py +1348 -0
- affinity/services/rate_limits.py +173 -0
- affinity/services/tasks.py +193 -0
- affinity/services/v1_only.py +2445 -0
- affinity/types.py +83 -0
- affinity_sdk-0.9.5.dist-info/METADATA +622 -0
- affinity_sdk-0.9.5.dist-info/RECORD +92 -0
- affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
- affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
- affinity_sdk-0.9.5.dist-info/licenses/LICENSE +21 -0
affinity/cli/paths.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from platformdirs import PlatformDirs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class CliPaths:
|
|
11
|
+
config_dir: Path
|
|
12
|
+
config_path: Path
|
|
13
|
+
cache_dir: Path
|
|
14
|
+
state_dir: Path
|
|
15
|
+
log_dir: Path
|
|
16
|
+
log_file: Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_paths(*, app_name: str = "xaffinity", app_author: str = "Affinity") -> CliPaths:
|
|
20
|
+
dirs = PlatformDirs(app_name, app_author)
|
|
21
|
+
config_dir = Path(dirs.user_config_dir)
|
|
22
|
+
cache_dir = Path(dirs.user_cache_dir)
|
|
23
|
+
state_dir = Path(dirs.user_state_dir)
|
|
24
|
+
log_dir = Path(getattr(dirs, "user_log_dir", "") or (state_dir / "logs"))
|
|
25
|
+
return CliPaths(
|
|
26
|
+
config_dir=config_dir,
|
|
27
|
+
config_path=config_dir / "config.toml",
|
|
28
|
+
cache_dir=cache_dir,
|
|
29
|
+
state_dir=state_dir,
|
|
30
|
+
log_dir=log_dir,
|
|
31
|
+
log_file=log_dir / "xaffinity.log",
|
|
32
|
+
)
|
affinity/cli/progress.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import AbstractContextManager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Literal, cast
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.progress import (
|
|
14
|
+
BarColumn,
|
|
15
|
+
DownloadColumn,
|
|
16
|
+
Progress,
|
|
17
|
+
TaskID,
|
|
18
|
+
TextColumn,
|
|
19
|
+
TimeRemainingColumn,
|
|
20
|
+
TransferSpeedColumn,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from affinity.progress import ProgressCallback, ProgressPhase
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
ProgressMode = Literal["auto", "always", "never"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class ProgressSettings:
|
|
32
|
+
mode: ProgressMode
|
|
33
|
+
quiet: bool
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProgressManager(AbstractContextManager["ProgressManager"]):
|
|
37
|
+
# Rate limit to stay under mcp-bash 100/min limit
|
|
38
|
+
# Using 0.65s (not 0.6s) to leave headroom for timing jitter (~92/min max)
|
|
39
|
+
_MIN_PROGRESS_INTERVAL = 0.65
|
|
40
|
+
|
|
41
|
+
def __init__(self, *, settings: ProgressSettings):
|
|
42
|
+
self._settings = settings
|
|
43
|
+
self._console = Console(file=sys.stderr)
|
|
44
|
+
self._progress: Progress | None = None
|
|
45
|
+
# JSON mode: emit NDJSON to stderr when not a TTY (for MCP consumption)
|
|
46
|
+
self._json_mode = (
|
|
47
|
+
settings.mode != "never" and not settings.quiet and not sys.stderr.isatty()
|
|
48
|
+
)
|
|
49
|
+
if self._json_mode:
|
|
50
|
+
logger.debug("Progress JSON mode enabled (stderr is not a TTY)")
|
|
51
|
+
# Use -inf so first call always succeeds regardless of time.monotonic() value
|
|
52
|
+
self._last_progress_time: float = float("-inf")
|
|
53
|
+
self._emitted_100: bool = False
|
|
54
|
+
self._current_task_description: str = ""
|
|
55
|
+
|
|
56
|
+
def __enter__(self) -> ProgressManager:
|
|
57
|
+
if self.enabled:
|
|
58
|
+
self._progress = Progress(
|
|
59
|
+
TextColumn("{task.description}"),
|
|
60
|
+
BarColumn(),
|
|
61
|
+
DownloadColumn(),
|
|
62
|
+
TransferSpeedColumn(),
|
|
63
|
+
TimeRemainingColumn(),
|
|
64
|
+
console=self._console,
|
|
65
|
+
transient=True,
|
|
66
|
+
)
|
|
67
|
+
self._progress.__enter__()
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
def _emit_json_progress(
|
|
71
|
+
self,
|
|
72
|
+
percent: int | None,
|
|
73
|
+
message: str,
|
|
74
|
+
current: int | None = None,
|
|
75
|
+
total: int | None = None,
|
|
76
|
+
*,
|
|
77
|
+
force: bool = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Emit NDJSON progress to stderr for MCP consumption.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
percent: Progress percentage (0-100), or None for indeterminate progress.
|
|
83
|
+
message: Human-readable progress message.
|
|
84
|
+
current: Current value (e.g., bytes transferred).
|
|
85
|
+
total: Total value (e.g., total bytes).
|
|
86
|
+
force: Bypass rate limiting (for completion messages).
|
|
87
|
+
"""
|
|
88
|
+
# Rate limit to avoid overwhelming MCP client (unless forced)
|
|
89
|
+
now = time.monotonic()
|
|
90
|
+
if not force and now - self._last_progress_time < self._MIN_PROGRESS_INTERVAL:
|
|
91
|
+
return
|
|
92
|
+
self._last_progress_time = now
|
|
93
|
+
|
|
94
|
+
# Include "type": "progress" to distinguish from error JSON on stderr
|
|
95
|
+
obj: dict[str, int | str | None] = {
|
|
96
|
+
"type": "progress",
|
|
97
|
+
"progress": percent,
|
|
98
|
+
"message": message,
|
|
99
|
+
}
|
|
100
|
+
if current is not None:
|
|
101
|
+
obj["current"] = current
|
|
102
|
+
if total is not None:
|
|
103
|
+
obj["total"] = total
|
|
104
|
+
# flush=True is CRITICAL: Python buffers stderr when not a TTY,
|
|
105
|
+
# so without flush, lines may not appear until buffer fills or process exits
|
|
106
|
+
print(json.dumps(obj), file=sys.stderr, flush=True)
|
|
107
|
+
|
|
108
|
+
def __exit__(
|
|
109
|
+
self,
|
|
110
|
+
exc_type: type[BaseException] | None,
|
|
111
|
+
exc: BaseException | None,
|
|
112
|
+
tb: TracebackType | None,
|
|
113
|
+
) -> None:
|
|
114
|
+
# Emit guaranteed 100% completion for JSON mode (bypass rate limiting)
|
|
115
|
+
if self._json_mode and not self._emitted_100 and exc_type is None:
|
|
116
|
+
self._emit_json_progress(100, f"{self._current_task_description} complete", force=True)
|
|
117
|
+
self._emitted_100 = True
|
|
118
|
+
# Existing Rich cleanup
|
|
119
|
+
if self._progress is not None:
|
|
120
|
+
self._progress.__exit__(exc_type, exc, tb)
|
|
121
|
+
self._progress = None
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def enabled(self) -> bool:
|
|
125
|
+
if self._settings.quiet:
|
|
126
|
+
return False
|
|
127
|
+
if self._settings.mode == "never":
|
|
128
|
+
return False
|
|
129
|
+
if self._settings.mode == "always":
|
|
130
|
+
return True
|
|
131
|
+
return sys.stderr.isatty()
|
|
132
|
+
|
|
133
|
+
def task(self, *, description: str, total_bytes: int | None) -> tuple[TaskID, ProgressCallback]:
|
|
134
|
+
# Track description for __exit__ 100% emission
|
|
135
|
+
self._current_task_description = description
|
|
136
|
+
self._emitted_100 = False
|
|
137
|
+
|
|
138
|
+
# JSON mode callback (when stderr is not a TTY)
|
|
139
|
+
if self._json_mode:
|
|
140
|
+
|
|
141
|
+
def json_callback(
|
|
142
|
+
bytes_transferred: int, total_bytes_arg: int | None, *, phase: ProgressPhase
|
|
143
|
+
) -> None:
|
|
144
|
+
del phase
|
|
145
|
+
# Compute percent from bytes (None total = indeterminate progress)
|
|
146
|
+
percent = bytes_transferred * 100 // total_bytes_arg if total_bytes_arg else None
|
|
147
|
+
self._emit_json_progress(percent, description, bytes_transferred, total_bytes_arg)
|
|
148
|
+
# Track if we've hit 100% (only for determinate progress)
|
|
149
|
+
if percent is not None and percent >= 100:
|
|
150
|
+
self._emitted_100 = True
|
|
151
|
+
|
|
152
|
+
# TaskID(0) is a no-op sentinel (progress not tracked in Rich)
|
|
153
|
+
return TaskID(0), cast(ProgressCallback, json_callback)
|
|
154
|
+
|
|
155
|
+
# Existing Rich progress bar logic
|
|
156
|
+
if not self.enabled or self._progress is None:
|
|
157
|
+
|
|
158
|
+
def noop(_: int, __: int | None, *, phase: ProgressPhase) -> None:
|
|
159
|
+
del phase
|
|
160
|
+
|
|
161
|
+
return TaskID(0), cast(ProgressCallback, noop)
|
|
162
|
+
|
|
163
|
+
task_id = self._progress.add_task(description, total=total_bytes)
|
|
164
|
+
|
|
165
|
+
def callback(bytes_transferred: int, total: int | None, *, phase: ProgressPhase) -> None:
|
|
166
|
+
del phase
|
|
167
|
+
if self._progress is None:
|
|
168
|
+
return
|
|
169
|
+
if total is not None:
|
|
170
|
+
self._progress.update(task_id, total=total)
|
|
171
|
+
self._progress.update(task_id, completed=bytes_transferred)
|
|
172
|
+
|
|
173
|
+
return task_id, cast(ProgressCallback, callback)
|
|
174
|
+
|
|
175
|
+
def advance(self, task_id: TaskID, advance: int = 1) -> None:
|
|
176
|
+
if self._progress is None:
|
|
177
|
+
return
|
|
178
|
+
self._progress.advance(task_id, advance)
|
|
179
|
+
|
|
180
|
+
def simple_status(self, text: str) -> None:
|
|
181
|
+
if not self.enabled:
|
|
182
|
+
return
|
|
183
|
+
self._console.print(text)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Query engine for the CLI.
|
|
2
|
+
|
|
3
|
+
This package provides a structured query language for querying Affinity data.
|
|
4
|
+
It is CLI-only and NOT part of the public SDK API.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from affinity.cli.query import parse_query, create_planner
|
|
8
|
+
|
|
9
|
+
result = parse_query({
|
|
10
|
+
"$version": "1.0",
|
|
11
|
+
"from": "persons",
|
|
12
|
+
"where": {"path": "email", "op": "contains", "value": "@acme.com"},
|
|
13
|
+
"limit": 50
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
planner = create_planner()
|
|
17
|
+
plan = planner.plan(result.query)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Phase 2 modules
|
|
21
|
+
from .aggregates import (
|
|
22
|
+
apply_having,
|
|
23
|
+
compute_aggregates,
|
|
24
|
+
group_and_aggregate,
|
|
25
|
+
)
|
|
26
|
+
from .dates import (
|
|
27
|
+
days_since,
|
|
28
|
+
days_until,
|
|
29
|
+
is_relative_date,
|
|
30
|
+
parse_date_value,
|
|
31
|
+
parse_relative_date,
|
|
32
|
+
)
|
|
33
|
+
from .exceptions import (
|
|
34
|
+
QueryError,
|
|
35
|
+
QueryExecutionError,
|
|
36
|
+
QueryInterruptedError,
|
|
37
|
+
QueryParseError,
|
|
38
|
+
QueryPlanError,
|
|
39
|
+
QuerySafetyLimitError,
|
|
40
|
+
QueryTimeoutError,
|
|
41
|
+
QueryValidationError,
|
|
42
|
+
)
|
|
43
|
+
from .executor import (
|
|
44
|
+
NullProgressCallback,
|
|
45
|
+
QueryExecutor,
|
|
46
|
+
QueryProgressCallback,
|
|
47
|
+
execute_query,
|
|
48
|
+
)
|
|
49
|
+
from .filters import (
|
|
50
|
+
compile_filter,
|
|
51
|
+
matches,
|
|
52
|
+
resolve_field_path,
|
|
53
|
+
)
|
|
54
|
+
from .models import (
|
|
55
|
+
AggregateFunc,
|
|
56
|
+
ExecutionPlan,
|
|
57
|
+
FilterCondition,
|
|
58
|
+
HavingClause,
|
|
59
|
+
OrderByClause,
|
|
60
|
+
PlanStep,
|
|
61
|
+
Query,
|
|
62
|
+
QueryResult,
|
|
63
|
+
WhereClause,
|
|
64
|
+
)
|
|
65
|
+
from .output import (
|
|
66
|
+
format_dry_run,
|
|
67
|
+
format_dry_run_json,
|
|
68
|
+
format_json,
|
|
69
|
+
format_table,
|
|
70
|
+
)
|
|
71
|
+
from .parser import (
|
|
72
|
+
CURRENT_VERSION,
|
|
73
|
+
SUPPORTED_ENTITIES,
|
|
74
|
+
SUPPORTED_VERSIONS,
|
|
75
|
+
ParseResult,
|
|
76
|
+
parse_query,
|
|
77
|
+
parse_query_from_file,
|
|
78
|
+
)
|
|
79
|
+
from .planner import QueryPlanner, create_planner
|
|
80
|
+
from .progress import (
|
|
81
|
+
NDJSONQueryProgress,
|
|
82
|
+
RichQueryProgress,
|
|
83
|
+
create_progress_callback,
|
|
84
|
+
)
|
|
85
|
+
from .schema import (
|
|
86
|
+
SCHEMA_REGISTRY,
|
|
87
|
+
EntitySchema,
|
|
88
|
+
RelationshipDef,
|
|
89
|
+
get_entity_relationships,
|
|
90
|
+
get_entity_schema,
|
|
91
|
+
get_relationship,
|
|
92
|
+
get_supported_entities,
|
|
93
|
+
is_valid_field_path,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
__all__ = [
|
|
97
|
+
# Exceptions
|
|
98
|
+
"QueryError",
|
|
99
|
+
"QueryParseError",
|
|
100
|
+
"QueryValidationError",
|
|
101
|
+
"QueryPlanError",
|
|
102
|
+
"QueryExecutionError",
|
|
103
|
+
"QueryInterruptedError",
|
|
104
|
+
"QueryTimeoutError",
|
|
105
|
+
"QuerySafetyLimitError",
|
|
106
|
+
# Models
|
|
107
|
+
"Query",
|
|
108
|
+
"WhereClause",
|
|
109
|
+
"FilterCondition",
|
|
110
|
+
"AggregateFunc",
|
|
111
|
+
"HavingClause",
|
|
112
|
+
"OrderByClause",
|
|
113
|
+
"PlanStep",
|
|
114
|
+
"ExecutionPlan",
|
|
115
|
+
"QueryResult",
|
|
116
|
+
# Parser
|
|
117
|
+
"parse_query",
|
|
118
|
+
"parse_query_from_file",
|
|
119
|
+
"ParseResult",
|
|
120
|
+
"CURRENT_VERSION",
|
|
121
|
+
"SUPPORTED_VERSIONS",
|
|
122
|
+
"SUPPORTED_ENTITIES",
|
|
123
|
+
# Planner
|
|
124
|
+
"QueryPlanner",
|
|
125
|
+
"create_planner",
|
|
126
|
+
# Schema
|
|
127
|
+
"SCHEMA_REGISTRY",
|
|
128
|
+
"EntitySchema",
|
|
129
|
+
"RelationshipDef",
|
|
130
|
+
"get_entity_schema",
|
|
131
|
+
"get_relationship",
|
|
132
|
+
"get_supported_entities",
|
|
133
|
+
"get_entity_relationships",
|
|
134
|
+
"is_valid_field_path",
|
|
135
|
+
# Filters
|
|
136
|
+
"compile_filter",
|
|
137
|
+
"matches",
|
|
138
|
+
"resolve_field_path",
|
|
139
|
+
# Dates
|
|
140
|
+
"parse_relative_date",
|
|
141
|
+
"parse_date_value",
|
|
142
|
+
"days_since",
|
|
143
|
+
"days_until",
|
|
144
|
+
"is_relative_date",
|
|
145
|
+
# Aggregates
|
|
146
|
+
"compute_aggregates",
|
|
147
|
+
"group_and_aggregate",
|
|
148
|
+
"apply_having",
|
|
149
|
+
# Executor
|
|
150
|
+
"QueryExecutor",
|
|
151
|
+
"QueryProgressCallback",
|
|
152
|
+
"NullProgressCallback",
|
|
153
|
+
"execute_query",
|
|
154
|
+
# Output
|
|
155
|
+
"format_json",
|
|
156
|
+
"format_table",
|
|
157
|
+
"format_dry_run",
|
|
158
|
+
"format_dry_run_json",
|
|
159
|
+
# Progress
|
|
160
|
+
"RichQueryProgress",
|
|
161
|
+
"NDJSONQueryProgress",
|
|
162
|
+
"create_progress_callback",
|
|
163
|
+
]
|