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.
Files changed (92) hide show
  1. affinity/__init__.py +139 -0
  2. affinity/cli/__init__.py +7 -0
  3. affinity/cli/click_compat.py +27 -0
  4. affinity/cli/commands/__init__.py +1 -0
  5. affinity/cli/commands/_entity_files_dump.py +219 -0
  6. affinity/cli/commands/_list_entry_fields.py +41 -0
  7. affinity/cli/commands/_v1_parsing.py +77 -0
  8. affinity/cli/commands/company_cmds.py +2139 -0
  9. affinity/cli/commands/completion_cmd.py +33 -0
  10. affinity/cli/commands/config_cmds.py +540 -0
  11. affinity/cli/commands/entry_cmds.py +33 -0
  12. affinity/cli/commands/field_cmds.py +413 -0
  13. affinity/cli/commands/interaction_cmds.py +875 -0
  14. affinity/cli/commands/list_cmds.py +3152 -0
  15. affinity/cli/commands/note_cmds.py +433 -0
  16. affinity/cli/commands/opportunity_cmds.py +1174 -0
  17. affinity/cli/commands/person_cmds.py +1980 -0
  18. affinity/cli/commands/query_cmd.py +444 -0
  19. affinity/cli/commands/relationship_strength_cmds.py +62 -0
  20. affinity/cli/commands/reminder_cmds.py +595 -0
  21. affinity/cli/commands/resolve_url_cmd.py +127 -0
  22. affinity/cli/commands/session_cmds.py +84 -0
  23. affinity/cli/commands/task_cmds.py +110 -0
  24. affinity/cli/commands/version_cmd.py +29 -0
  25. affinity/cli/commands/whoami_cmd.py +36 -0
  26. affinity/cli/config.py +108 -0
  27. affinity/cli/context.py +749 -0
  28. affinity/cli/csv_utils.py +195 -0
  29. affinity/cli/date_utils.py +42 -0
  30. affinity/cli/decorators.py +77 -0
  31. affinity/cli/errors.py +28 -0
  32. affinity/cli/field_utils.py +355 -0
  33. affinity/cli/formatters.py +551 -0
  34. affinity/cli/help_json.py +283 -0
  35. affinity/cli/logging.py +100 -0
  36. affinity/cli/main.py +261 -0
  37. affinity/cli/options.py +53 -0
  38. affinity/cli/paths.py +32 -0
  39. affinity/cli/progress.py +183 -0
  40. affinity/cli/query/__init__.py +163 -0
  41. affinity/cli/query/aggregates.py +357 -0
  42. affinity/cli/query/dates.py +194 -0
  43. affinity/cli/query/exceptions.py +147 -0
  44. affinity/cli/query/executor.py +1236 -0
  45. affinity/cli/query/filters.py +248 -0
  46. affinity/cli/query/models.py +333 -0
  47. affinity/cli/query/output.py +331 -0
  48. affinity/cli/query/parser.py +619 -0
  49. affinity/cli/query/planner.py +430 -0
  50. affinity/cli/query/progress.py +270 -0
  51. affinity/cli/query/schema.py +439 -0
  52. affinity/cli/render.py +1589 -0
  53. affinity/cli/resolve.py +222 -0
  54. affinity/cli/resolvers.py +249 -0
  55. affinity/cli/results.py +308 -0
  56. affinity/cli/runner.py +218 -0
  57. affinity/cli/serialization.py +65 -0
  58. affinity/cli/session_cache.py +276 -0
  59. affinity/cli/types.py +70 -0
  60. affinity/client.py +771 -0
  61. affinity/clients/__init__.py +19 -0
  62. affinity/clients/http.py +3664 -0
  63. affinity/clients/pipeline.py +165 -0
  64. affinity/compare.py +501 -0
  65. affinity/downloads.py +114 -0
  66. affinity/exceptions.py +615 -0
  67. affinity/filters.py +1128 -0
  68. affinity/hooks.py +198 -0
  69. affinity/inbound_webhooks.py +302 -0
  70. affinity/models/__init__.py +163 -0
  71. affinity/models/entities.py +798 -0
  72. affinity/models/pagination.py +513 -0
  73. affinity/models/rate_limit_snapshot.py +48 -0
  74. affinity/models/secondary.py +413 -0
  75. affinity/models/types.py +663 -0
  76. affinity/policies.py +40 -0
  77. affinity/progress.py +22 -0
  78. affinity/py.typed +0 -0
  79. affinity/services/__init__.py +42 -0
  80. affinity/services/companies.py +1286 -0
  81. affinity/services/lists.py +1892 -0
  82. affinity/services/opportunities.py +1330 -0
  83. affinity/services/persons.py +1348 -0
  84. affinity/services/rate_limits.py +173 -0
  85. affinity/services/tasks.py +193 -0
  86. affinity/services/v1_only.py +2445 -0
  87. affinity/types.py +83 -0
  88. affinity_sdk-0.9.5.dist-info/METADATA +622 -0
  89. affinity_sdk-0.9.5.dist-info/RECORD +92 -0
  90. affinity_sdk-0.9.5.dist-info/WHEEL +4 -0
  91. affinity_sdk-0.9.5.dist-info/entry_points.txt +2 -0
  92. 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
+ )
@@ -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
+ ]