attio 0.2.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -45
- data/.gitignore +1 -0
- data/CHANGELOG.md +69 -0
- data/CLAUDE.md +391 -0
- data/Gemfile.lock +1 -1
- data/README.md +370 -24
- data/lib/attio/circuit_breaker.rb +299 -0
- data/lib/attio/client.rb +43 -1
- data/lib/attio/connection_pool.rb +190 -35
- data/lib/attio/enhanced_client.rb +257 -0
- data/lib/attio/errors.rb +30 -2
- data/lib/attio/http_client.rb +58 -3
- data/lib/attio/observability.rb +424 -0
- data/lib/attio/rate_limiter.rb +212 -0
- data/lib/attio/resources/base.rb +70 -2
- data/lib/attio/resources/bulk.rb +290 -0
- data/lib/attio/resources/deals.rb +183 -0
- data/lib/attio/resources/records.rb +29 -2
- data/lib/attio/resources/workspace_members.rb +103 -0
- data/lib/attio/version.rb +1 -1
- data/lib/attio/webhooks.rb +220 -0
- data/lib/attio.rb +12 -0
- metadata +10 -1
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Attio Ruby Client
|
2
2
|
|
3
3
|
[](https://github.com/idl3/attio/actions/workflows/tests.yml)
|
4
|
-
[](https://github.com/idl3/attio/tree/master/spec)
|
5
5
|
[](https://idl3.github.io/attio)
|
6
6
|
[](https://badge.fury.io/rb/attio)
|
7
|
-
[](https://github.com/idl3/attio/tree/master/spec)
|
8
8
|
|
9
9
|
Ruby client for the [Attio CRM API](https://developers.attio.com/). This library provides easy access to the Attio API, allowing you to manage records, objects, lists, and more.
|
10
10
|
|
@@ -84,22 +84,21 @@ client = Attio::Client.new(api_key: 'your-api-key', timeout: 60)
|
|
84
84
|
# List all people
|
85
85
|
people = client.records.list(object: 'people')
|
86
86
|
|
87
|
-
# List with
|
87
|
+
# List with filtering and sorting
|
88
88
|
filtered_people = client.records.list(
|
89
89
|
object: 'people',
|
90
|
-
|
91
|
-
name: { contains: 'John' }
|
92
|
-
company: { target_object: 'companies', target_record_id: 'company-123' }
|
90
|
+
filter: {
|
91
|
+
name: { $contains: 'John' }
|
93
92
|
},
|
94
|
-
|
93
|
+
sort: 'created_at.desc',
|
94
|
+
limit: 50,
|
95
|
+
offset: 0
|
95
96
|
)
|
96
97
|
|
97
|
-
# List with
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
limit: 25
|
102
|
-
)
|
98
|
+
# List all records with automatic pagination
|
99
|
+
client.records.list_all(object: 'people', page_size: 50).each do |person|
|
100
|
+
puts person['name']
|
101
|
+
end
|
103
102
|
```
|
104
103
|
|
105
104
|
#### Creating Records
|
@@ -361,6 +360,293 @@ users = client.users.list
|
|
361
360
|
user = client.users.me
|
362
361
|
```
|
363
362
|
|
363
|
+
### Advanced Features
|
364
|
+
|
365
|
+
#### Workspace Members
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
# List workspace members
|
369
|
+
members = client.workspace_members.list
|
370
|
+
|
371
|
+
# Invite a new member
|
372
|
+
invitation = client.workspace_members.invite(
|
373
|
+
email: 'new.member@example.com',
|
374
|
+
role: 'member' # admin, member, or guest
|
375
|
+
)
|
376
|
+
|
377
|
+
# Update member permissions
|
378
|
+
client.workspace_members.update(
|
379
|
+
member_id: 'user-123',
|
380
|
+
data: { role: 'admin' }
|
381
|
+
)
|
382
|
+
|
383
|
+
# Remove a member
|
384
|
+
client.workspace_members.remove(member_id: 'user-123')
|
385
|
+
```
|
386
|
+
|
387
|
+
#### Deals
|
388
|
+
|
389
|
+
```ruby
|
390
|
+
# List all deals
|
391
|
+
deals = client.deals.list
|
392
|
+
|
393
|
+
# Create a new deal
|
394
|
+
deal = client.deals.create(
|
395
|
+
data: {
|
396
|
+
name: 'Enterprise Contract',
|
397
|
+
value: 50000,
|
398
|
+
stage_id: 'stage-negotiation',
|
399
|
+
company_id: 'company-123'
|
400
|
+
}
|
401
|
+
)
|
402
|
+
|
403
|
+
# Update deal stage
|
404
|
+
client.deals.update_stage(id: 'deal-123', stage_id: 'stage-won')
|
405
|
+
|
406
|
+
# Mark deal as won/lost
|
407
|
+
client.deals.mark_won(id: 'deal-123', won_date: Date.today)
|
408
|
+
client.deals.mark_lost(id: 'deal-123', lost_reason: 'Budget constraints')
|
409
|
+
|
410
|
+
# List deals by various criteria
|
411
|
+
pipeline_deals = client.deals.list_by_stage(stage_id: 'stage-proposal')
|
412
|
+
company_deals = client.deals.list_by_company(company_id: 'company-123')
|
413
|
+
my_deals = client.deals.list_by_owner(owner_id: 'user-456')
|
414
|
+
```
|
415
|
+
|
416
|
+
#### Bulk Operations
|
417
|
+
|
418
|
+
```ruby
|
419
|
+
# Bulk create records
|
420
|
+
results = client.bulk.create_records(
|
421
|
+
object: 'people',
|
422
|
+
records: [
|
423
|
+
{ name: 'John Doe', email: 'john@example.com' },
|
424
|
+
{ name: 'Jane Smith', email: 'jane@example.com' },
|
425
|
+
# ... up to 100 records per batch
|
426
|
+
]
|
427
|
+
)
|
428
|
+
|
429
|
+
# Bulk update records
|
430
|
+
results = client.bulk.update_records(
|
431
|
+
object: 'companies',
|
432
|
+
updates: [
|
433
|
+
{ id: 'company-1', data: { status: 'active' } },
|
434
|
+
{ id: 'company-2', data: { status: 'inactive' } }
|
435
|
+
]
|
436
|
+
)
|
437
|
+
|
438
|
+
# Bulk upsert (create or update based on matching)
|
439
|
+
results = client.bulk.upsert_records(
|
440
|
+
object: 'people',
|
441
|
+
match_attribute: 'email',
|
442
|
+
records: [
|
443
|
+
{ email: 'john@example.com', name: 'John Updated' },
|
444
|
+
{ email: 'new@example.com', name: 'New Person' }
|
445
|
+
]
|
446
|
+
)
|
447
|
+
```
|
448
|
+
|
449
|
+
#### Rate Limiting
|
450
|
+
|
451
|
+
```ruby
|
452
|
+
# Initialize client with custom rate limiter
|
453
|
+
limiter = Attio::RateLimiter.new(
|
454
|
+
max_requests: 100,
|
455
|
+
window_seconds: 60,
|
456
|
+
max_retries: 3
|
457
|
+
)
|
458
|
+
client.rate_limiter = limiter
|
459
|
+
|
460
|
+
# Execute with rate limiting
|
461
|
+
limiter.execute { client.records.list(object: 'people') }
|
462
|
+
|
463
|
+
# Queue requests for later processing
|
464
|
+
limiter.queue_request(priority: 1) { important_operation }
|
465
|
+
limiter.queue_request(priority: 5) { less_important_operation }
|
466
|
+
|
467
|
+
# Process queued requests
|
468
|
+
results = limiter.process_queue(max_per_batch: 10)
|
469
|
+
|
470
|
+
# Check rate limit status
|
471
|
+
status = limiter.status
|
472
|
+
puts "Remaining: #{status[:remaining]}/#{status[:limit]}"
|
473
|
+
```
|
474
|
+
|
475
|
+
## Enterprise Features
|
476
|
+
|
477
|
+
The gem includes advanced enterprise features for production use:
|
478
|
+
|
479
|
+
### Enhanced Client
|
480
|
+
|
481
|
+
The `EnhancedClient` provides production-ready features including connection pooling, circuit breaker, observability, and webhook support:
|
482
|
+
|
483
|
+
```ruby
|
484
|
+
# Create an enhanced client with all features
|
485
|
+
client = Attio.enhanced_client(
|
486
|
+
api_key: ENV['ATTIO_API_KEY'],
|
487
|
+
connection_pool: {
|
488
|
+
size: 10, # Pool size
|
489
|
+
timeout: 5 # Checkout timeout
|
490
|
+
},
|
491
|
+
circuit_breaker: {
|
492
|
+
threshold: 5, # Failures before opening
|
493
|
+
timeout: 30, # Recovery timeout in seconds
|
494
|
+
half_open_requests: 2
|
495
|
+
},
|
496
|
+
instrumentation: {
|
497
|
+
logger: Rails.logger,
|
498
|
+
metrics: :datadog, # or :statsd, :prometheus, :opentelemetry
|
499
|
+
traces: :datadog # or :opentelemetry
|
500
|
+
},
|
501
|
+
webhook_secret: ENV['ATTIO_WEBHOOK_SECRET']
|
502
|
+
)
|
503
|
+
|
504
|
+
# Use it like a regular client
|
505
|
+
records = client.records.list(object: 'people')
|
506
|
+
|
507
|
+
# Execute with circuit breaker protection
|
508
|
+
client.execute(endpoint: 'api/records') do
|
509
|
+
client.records.create(object: 'people', data: { name: 'John' })
|
510
|
+
end
|
511
|
+
|
512
|
+
# Check health of all components
|
513
|
+
health = client.health_check
|
514
|
+
# => { api: true, pool: true, circuit_breaker: :healthy, rate_limiter: true }
|
515
|
+
|
516
|
+
# Get statistics
|
517
|
+
stats = client.stats
|
518
|
+
# => { pool: { size: 10, available: 7 }, circuit_breaker: { state: :closed, requests: 100 } }
|
519
|
+
```
|
520
|
+
|
521
|
+
### Connection Pooling
|
522
|
+
|
523
|
+
Efficient connection management with thread-safe pooling:
|
524
|
+
|
525
|
+
```ruby
|
526
|
+
pool = Attio::ConnectionPool.new(size: 5, timeout: 2) do
|
527
|
+
Attio::HttpClient.new(
|
528
|
+
base_url: 'https://api.attio.com/v2',
|
529
|
+
headers: { 'Authorization' => "Bearer #{api_key}" }
|
530
|
+
)
|
531
|
+
end
|
532
|
+
|
533
|
+
# Use connections from the pool
|
534
|
+
pool.with do |connection|
|
535
|
+
connection.get('records')
|
536
|
+
end
|
537
|
+
|
538
|
+
# Check pool status
|
539
|
+
stats = pool.stats
|
540
|
+
# => { size: 5, available: 3, allocated: 2 }
|
541
|
+
|
542
|
+
# Graceful shutdown
|
543
|
+
pool.shutdown
|
544
|
+
```
|
545
|
+
|
546
|
+
### Circuit Breaker
|
547
|
+
|
548
|
+
Fault tolerance with circuit breaker pattern:
|
549
|
+
|
550
|
+
```ruby
|
551
|
+
breaker = Attio::CircuitBreaker.new(
|
552
|
+
threshold: 5, # Open after 5 failures
|
553
|
+
timeout: 30, # Reset after 30 seconds
|
554
|
+
half_open_requests: 2
|
555
|
+
)
|
556
|
+
|
557
|
+
# Execute with protection
|
558
|
+
result = breaker.execute do
|
559
|
+
risky_api_call
|
560
|
+
end
|
561
|
+
|
562
|
+
# Monitor state changes
|
563
|
+
breaker.on_state_change = ->(old_state, new_state) {
|
564
|
+
puts "Circuit breaker: #{old_state} -> #{new_state}"
|
565
|
+
}
|
566
|
+
|
567
|
+
# Check current state
|
568
|
+
breaker.state # => :closed, :open, or :half_open
|
569
|
+
breaker.stats # => { requests: 100, failures: 2, success_rate: 0.98 }
|
570
|
+
```
|
571
|
+
|
572
|
+
### Observability
|
573
|
+
|
574
|
+
Comprehensive monitoring with multiple backend support:
|
575
|
+
|
576
|
+
```ruby
|
577
|
+
# Initialize with your preferred backend
|
578
|
+
instrumentation = Attio::Observability::Instrumentation.new(
|
579
|
+
logger: Logger.new(STDOUT),
|
580
|
+
metrics_backend: :datadog, # :statsd, :prometheus, :memory
|
581
|
+
trace_backend: :opentelemetry # :datadog, :memory
|
582
|
+
)
|
583
|
+
|
584
|
+
# Record API calls
|
585
|
+
instrumentation.record_api_call(
|
586
|
+
method: :post,
|
587
|
+
path: '/records',
|
588
|
+
duration: 0.125,
|
589
|
+
status: 200
|
590
|
+
)
|
591
|
+
|
592
|
+
# Record rate limits
|
593
|
+
instrumentation.record_rate_limit(
|
594
|
+
remaining: 450,
|
595
|
+
limit: 500,
|
596
|
+
reset_at: Time.now + 3600
|
597
|
+
)
|
598
|
+
|
599
|
+
# Record circuit breaker state changes
|
600
|
+
instrumentation.record_circuit_breaker(
|
601
|
+
endpoint: 'api/records',
|
602
|
+
old_state: :closed,
|
603
|
+
new_state: :open
|
604
|
+
)
|
605
|
+
|
606
|
+
# Track pool statistics
|
607
|
+
instrumentation.record_pool_stats(
|
608
|
+
size: 10,
|
609
|
+
available: 7,
|
610
|
+
allocated: 3
|
611
|
+
)
|
612
|
+
```
|
613
|
+
|
614
|
+
### Webhook Processing
|
615
|
+
|
616
|
+
Secure webhook handling with signature verification:
|
617
|
+
|
618
|
+
```ruby
|
619
|
+
# Initialize webhook handler
|
620
|
+
webhooks = Attio::Webhooks.new(secret: ENV['ATTIO_WEBHOOK_SECRET'])
|
621
|
+
|
622
|
+
# Register event handlers
|
623
|
+
webhooks.on('record.created') do |event|
|
624
|
+
puts "New record: #{event.data['id']}"
|
625
|
+
end
|
626
|
+
|
627
|
+
webhooks.on_any do |event|
|
628
|
+
puts "Event: #{event.type}"
|
629
|
+
end
|
630
|
+
|
631
|
+
# Process incoming webhook
|
632
|
+
begin
|
633
|
+
event = webhooks.process(
|
634
|
+
request.body.read,
|
635
|
+
request.headers
|
636
|
+
)
|
637
|
+
render json: { status: 'ok' }
|
638
|
+
rescue Attio::Webhooks::InvalidSignatureError => e
|
639
|
+
render json: { error: 'Invalid signature' }, status: 401
|
640
|
+
end
|
641
|
+
|
642
|
+
# Development webhook server
|
643
|
+
server = Attio::WebhookServer.new(port: 3001, secret: 'test_secret')
|
644
|
+
server.webhooks.on('record.created') do |event|
|
645
|
+
puts "Received: #{event.inspect}"
|
646
|
+
end
|
647
|
+
server.start # Starts WEBrick server for testing
|
648
|
+
```
|
649
|
+
|
364
650
|
### Error Handling
|
365
651
|
|
366
652
|
The client will raise appropriate exceptions for different error conditions:
|
@@ -390,16 +676,35 @@ end
|
|
390
676
|
|
391
677
|
This client supports all major Attio API endpoints:
|
392
678
|
|
393
|
-
|
394
|
-
- ✅
|
395
|
-
- ✅
|
396
|
-
- ✅
|
397
|
-
- ✅ Attributes
|
398
|
-
- ✅
|
399
|
-
- ✅
|
400
|
-
|
401
|
-
|
402
|
-
- ✅
|
679
|
+
### Core Resources
|
680
|
+
- ✅ **Records** - Full CRUD operations, querying with filters and sorting
|
681
|
+
- ✅ **Objects** - List, get schema information
|
682
|
+
- ✅ **Lists** - List, get entries, manage list entries
|
683
|
+
- ✅ **Attributes** - List, create, update custom attributes
|
684
|
+
- ✅ **Workspaces** - List, get current workspace
|
685
|
+
- ✅ **Users** - List, get current user
|
686
|
+
|
687
|
+
### Collaboration Features
|
688
|
+
- ✅ **Comments** - CRUD operations, emoji reactions on records and threads
|
689
|
+
- ✅ **Threads** - CRUD operations, participant management, status control
|
690
|
+
- ✅ **Tasks** - CRUD operations, assignment, completion tracking
|
691
|
+
- ✅ **Notes** - CRUD operations on records
|
692
|
+
|
693
|
+
### Sales & CRM
|
694
|
+
- ✅ **Deals** - Pipeline management, stage tracking, win/loss tracking
|
695
|
+
- ✅ **Workspace Members** - Member management, invitations, permissions
|
696
|
+
|
697
|
+
### Advanced Features
|
698
|
+
- ✅ **Bulk Operations** - Batch create/update/delete with automatic batching (1000 items max)
|
699
|
+
- ✅ **Rate Limiting** - Intelligent retry with exponential backoff and request queuing
|
700
|
+
|
701
|
+
### Enterprise Features
|
702
|
+
- ✅ **Enhanced Client** - Production-ready client with pooling, circuit breaker, and observability
|
703
|
+
- ✅ **Connection Pooling** - Thread-safe connection management with configurable pool size
|
704
|
+
- ✅ **Circuit Breaker** - Fault tolerance with automatic recovery and state monitoring
|
705
|
+
- ✅ **Observability** - Metrics and tracing with StatsD, Datadog, Prometheus, OpenTelemetry support
|
706
|
+
- ✅ **Webhook Processing** - Secure webhook handling with HMAC signature verification
|
707
|
+
- ✅ **Middleware** - Request/response instrumentation for monitoring
|
403
708
|
|
404
709
|
## Development
|
405
710
|
|
@@ -426,10 +731,51 @@ bundle exec rake docs:serve
|
|
426
731
|
|
427
732
|
### Code Coverage
|
428
733
|
|
734
|
+
The gem maintains 100% test coverage across all features:
|
735
|
+
|
429
736
|
```bash
|
430
|
-
|
737
|
+
# Run tests with coverage report
|
738
|
+
bundle exec rspec
|
739
|
+
|
740
|
+
# View detailed coverage report
|
741
|
+
open coverage/index.html
|
431
742
|
```
|
432
743
|
|
744
|
+
Current stats:
|
745
|
+
- **Test Coverage**: 100% (1311/1311 lines)
|
746
|
+
- **Test Count**: 590 tests
|
747
|
+
- **RuboCop**: 0 violations
|
748
|
+
|
749
|
+
## Migration from v0.3.0 to v0.4.0
|
750
|
+
|
751
|
+
### Breaking Changes
|
752
|
+
|
753
|
+
1. **Meta API Removed**: The Meta resource was completely fake and has been removed.
|
754
|
+
```ruby
|
755
|
+
# OLD (will not work)
|
756
|
+
client.meta.identify
|
757
|
+
|
758
|
+
# NEW - use a real endpoint if needed
|
759
|
+
# No direct replacement - Meta API didn't exist in Attio
|
760
|
+
```
|
761
|
+
|
762
|
+
2. **Webhook Headers Fixed**: Header names no longer have X- prefix.
|
763
|
+
```ruby
|
764
|
+
# OLD
|
765
|
+
headers["X-Attio-Signature"]
|
766
|
+
|
767
|
+
# NEW
|
768
|
+
headers["Attio-Signature"]
|
769
|
+
```
|
770
|
+
|
771
|
+
3. **Records List Method**: Now uses GET instead of POST internally (no API change needed).
|
772
|
+
|
773
|
+
### New Features
|
774
|
+
|
775
|
+
- **Rate Limiting**: Now automatically enforced
|
776
|
+
- **Pagination**: Use `list_all` for automatic pagination
|
777
|
+
- **Filtering**: Full support for Attio's filter syntax
|
778
|
+
|
433
779
|
## Contributing
|
434
780
|
|
435
781
|
Bug reports and pull requests are welcome on GitHub at https://github.com/idl3/attio.
|