getjobber-cli 1.0.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.
@@ -0,0 +1,5 @@
1
+ """GetJobber CLI - Terminal access to GetJobber CRM API."""
2
+
3
+ from getjobber_cli.constants import APP_VERSION
4
+
5
+ __version__ = APP_VERSION
@@ -0,0 +1,6 @@
1
+ """Entry point for GetJobber CLI."""
2
+
3
+ from getjobber_cli.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
File without changes
@@ -0,0 +1,149 @@
1
+ """GraphQL client for GetJobber API."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from gql import Client, gql
6
+ from gql.transport.requests import RequestsHTTPTransport
7
+
8
+ from getjobber_cli.constants import API_BASE_URL, DEFAULT_TIMEOUT
9
+ from getjobber_cli.utils.errors import GraphQLError, JobberAPIError, NotAuthenticatedError
10
+
11
+
12
+ def create_client(access_token: str, api_url: str = API_BASE_URL) -> Client:
13
+ """Create configured GraphQL client.
14
+
15
+ Args:
16
+ access_token: OAuth access token for authentication.
17
+ api_url: API endpoint URL.
18
+
19
+ Returns:
20
+ Configured GQL Client instance.
21
+
22
+ Raises:
23
+ NotAuthenticatedError: If access token is not provided.
24
+ """
25
+ if not access_token:
26
+ raise NotAuthenticatedError()
27
+
28
+ # Configure transport with authentication
29
+ # Jobber requires X-JOBBER-GRAPHQL-VERSION; pin to a known stable schema date
30
+ transport = RequestsHTTPTransport(
31
+ url=api_url,
32
+ headers={
33
+ "Authorization": f"Bearer {access_token}",
34
+ "Content-Type": "application/json",
35
+ "X-JOBBER-GRAPHQL-VERSION": "2025-04-16",
36
+ },
37
+ timeout=DEFAULT_TIMEOUT,
38
+ verify=True,
39
+ retries=3,
40
+ )
41
+
42
+ # Create client with schema introspection
43
+ client = Client(
44
+ transport=transport,
45
+ fetch_schema_from_transport=False, # Disable for now, can be enabled later
46
+ )
47
+
48
+ return client
49
+
50
+
51
+ def execute_query(client: Client, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict:
52
+ """Execute a GraphQL query.
53
+
54
+ Args:
55
+ client: GQL Client instance.
56
+ query: GraphQL query string.
57
+ variables: Optional query variables.
58
+
59
+ Returns:
60
+ Query result dictionary.
61
+
62
+ Raises:
63
+ GraphQLError: If query execution fails.
64
+ JobberAPIError: If API returns an error.
65
+ """
66
+ try:
67
+ # Parse query
68
+ parsed_query = gql(query)
69
+
70
+ # Execute query
71
+ result = client.execute(parsed_query, variable_values=variables)
72
+ return result
73
+
74
+ except Exception as e:
75
+ error_message = str(e)
76
+
77
+ # Check if it's a GraphQL error with structured errors
78
+ if hasattr(e, "errors") and e.errors:
79
+ raise GraphQLError("GraphQL query failed", errors=e.errors)
80
+
81
+ # Check for authentication errors
82
+ if "401" in error_message or "Unauthorized" in error_message:
83
+ raise NotAuthenticatedError("Authentication failed. Please login again.")
84
+
85
+ # Check for rate limiting
86
+ if "429" in error_message or "rate limit" in error_message.lower():
87
+ from getjobber_cli.utils.errors import RateLimitError
88
+
89
+ raise RateLimitError()
90
+
91
+ # Generic GraphQL error
92
+ raise GraphQLError(error_message)
93
+
94
+
95
+ def execute_mutation(
96
+ client: Client, mutation: str, variables: Optional[Dict[str, Any]] = None
97
+ ) -> Dict:
98
+ """Execute a GraphQL mutation.
99
+
100
+ Args:
101
+ client: GQL Client instance.
102
+ mutation: GraphQL mutation string.
103
+ variables: Optional mutation variables.
104
+
105
+ Returns:
106
+ Mutation result dictionary.
107
+
108
+ Raises:
109
+ GraphQLError: If mutation execution fails.
110
+ JobberAPIError: If API returns an error.
111
+ """
112
+ # Mutations use the same execution as queries
113
+ return execute_query(client, mutation, variables)
114
+
115
+
116
+ class GraphQLClient:
117
+ """Wrapper class for GraphQL client operations."""
118
+
119
+ def __init__(self, access_token: str):
120
+ """Initialize GraphQL client wrapper.
121
+
122
+ Args:
123
+ access_token: OAuth access token.
124
+ """
125
+ self.client = create_client(access_token)
126
+
127
+ def query(self, query: str, variables: Optional[Dict[str, Any]] = None) -> Dict:
128
+ """Execute a GraphQL query.
129
+
130
+ Args:
131
+ query: GraphQL query string.
132
+ variables: Optional query variables.
133
+
134
+ Returns:
135
+ Query result dictionary.
136
+ """
137
+ return execute_query(self.client, query, variables)
138
+
139
+ def mutate(self, mutation: str, variables: Optional[Dict[str, Any]] = None) -> Dict:
140
+ """Execute a GraphQL mutation.
141
+
142
+ Args:
143
+ mutation: GraphQL mutation string.
144
+ variables: Optional mutation variables.
145
+
146
+ Returns:
147
+ Mutation result dictionary.
148
+ """
149
+ return execute_mutation(self.client, mutation, variables)
@@ -0,0 +1,251 @@
1
+ """Pre-built GraphQL mutations for GetJobber API."""
2
+
3
+ # Client Mutations
4
+ CREATE_CLIENT = """
5
+ mutation CreateClient($input: ClientInput!) {
6
+ clientCreate(input: $input) {
7
+ client {
8
+ id
9
+ firstName
10
+ lastName
11
+ companyName
12
+ email
13
+ phoneNumber
14
+ createdAt
15
+ }
16
+ userErrors {
17
+ message
18
+ path
19
+ }
20
+ }
21
+ }
22
+ """
23
+
24
+ UPDATE_CLIENT = """
25
+ mutation UpdateClient($id: ID!, $input: ClientInput!) {
26
+ clientUpdate(id: $id, input: $input) {
27
+ client {
28
+ id
29
+ firstName
30
+ lastName
31
+ companyName
32
+ email
33
+ phoneNumber
34
+ updatedAt
35
+ }
36
+ userErrors {
37
+ message
38
+ path
39
+ }
40
+ }
41
+ }
42
+ """
43
+
44
+ DELETE_CLIENT = """
45
+ mutation DeleteClient($id: ID!) {
46
+ clientArchive(id: $id) {
47
+ client {
48
+ id
49
+ }
50
+ userErrors {
51
+ message
52
+ path
53
+ }
54
+ }
55
+ }
56
+ """
57
+
58
+ # Job Mutations
59
+ CREATE_JOB = """
60
+ mutation CreateJob($input: JobInput!) {
61
+ jobCreate(input: $input) {
62
+ job {
63
+ id
64
+ title
65
+ jobNumber
66
+ status
67
+ client {
68
+ id
69
+ firstName
70
+ lastName
71
+ }
72
+ createdAt
73
+ }
74
+ userErrors {
75
+ message
76
+ path
77
+ }
78
+ }
79
+ }
80
+ """
81
+
82
+ UPDATE_JOB = """
83
+ mutation UpdateJob($id: ID!, $input: JobInput!) {
84
+ jobUpdate(id: $id, input: $input) {
85
+ job {
86
+ id
87
+ title
88
+ jobNumber
89
+ status
90
+ updatedAt
91
+ }
92
+ userErrors {
93
+ message
94
+ path
95
+ }
96
+ }
97
+ }
98
+ """
99
+
100
+ COMPLETE_JOB = """
101
+ mutation CompleteJob($id: ID!) {
102
+ jobComplete(id: $id) {
103
+ job {
104
+ id
105
+ title
106
+ status
107
+ completedAt
108
+ }
109
+ userErrors {
110
+ message
111
+ path
112
+ }
113
+ }
114
+ }
115
+ """
116
+
117
+ # Quote Mutations
118
+ CREATE_QUOTE = """
119
+ mutation CreateQuote($input: QuoteInput!) {
120
+ quoteCreate(input: $input) {
121
+ quote {
122
+ id
123
+ quoteNumber
124
+ title
125
+ status
126
+ client {
127
+ id
128
+ firstName
129
+ lastName
130
+ }
131
+ totalAmount
132
+ createdAt
133
+ }
134
+ userErrors {
135
+ message
136
+ path
137
+ }
138
+ }
139
+ }
140
+ """
141
+
142
+ UPDATE_QUOTE = """
143
+ mutation UpdateQuote($id: ID!, $input: QuoteInput!) {
144
+ quoteUpdate(id: $id, input: $input) {
145
+ quote {
146
+ id
147
+ quoteNumber
148
+ title
149
+ status
150
+ updatedAt
151
+ }
152
+ userErrors {
153
+ message
154
+ path
155
+ }
156
+ }
157
+ }
158
+ """
159
+
160
+ SEND_QUOTE = """
161
+ mutation SendQuote($id: ID!) {
162
+ quoteSend(id: $id) {
163
+ quote {
164
+ id
165
+ quoteNumber
166
+ status
167
+ sentAt
168
+ }
169
+ userErrors {
170
+ message
171
+ path
172
+ }
173
+ }
174
+ }
175
+ """
176
+
177
+ APPROVE_QUOTE = """
178
+ mutation ApproveQuote($id: ID!) {
179
+ quoteApprove(id: $id) {
180
+ quote {
181
+ id
182
+ quoteNumber
183
+ status
184
+ }
185
+ userErrors {
186
+ message
187
+ path
188
+ }
189
+ }
190
+ }
191
+ """
192
+
193
+ # Invoice Mutations
194
+ CREATE_INVOICE = """
195
+ mutation CreateInvoice($input: InvoiceInput!) {
196
+ invoiceCreate(input: $input) {
197
+ invoice {
198
+ id
199
+ invoiceNumber
200
+ subject
201
+ status
202
+ client {
203
+ id
204
+ firstName
205
+ lastName
206
+ }
207
+ totalAmount
208
+ createdAt
209
+ }
210
+ userErrors {
211
+ message
212
+ path
213
+ }
214
+ }
215
+ }
216
+ """
217
+
218
+ UPDATE_INVOICE = """
219
+ mutation UpdateInvoice($id: ID!, $input: InvoiceInput!) {
220
+ invoiceUpdate(id: $id, input: $input) {
221
+ invoice {
222
+ id
223
+ invoiceNumber
224
+ subject
225
+ status
226
+ updatedAt
227
+ }
228
+ userErrors {
229
+ message
230
+ path
231
+ }
232
+ }
233
+ }
234
+ """
235
+
236
+ SEND_INVOICE = """
237
+ mutation SendInvoice($id: ID!) {
238
+ invoiceSend(id: $id) {
239
+ invoice {
240
+ id
241
+ invoiceNumber
242
+ status
243
+ sentAt
244
+ }
245
+ userErrors {
246
+ message
247
+ path
248
+ }
249
+ }
250
+ }
251
+ """
@@ -0,0 +1,264 @@
1
+ """Pre-built GraphQL queries for GetJobber API."""
2
+
3
+ # Client Queries
4
+ LIST_CLIENTS = """
5
+ query ListClients($first: Int, $after: String) {
6
+ clients(first: $first, after: $after) {
7
+ nodes {
8
+ id
9
+ firstName
10
+ lastName
11
+ companyName
12
+ email
13
+ phone
14
+ createdAt
15
+ updatedAt
16
+ }
17
+ pageInfo {
18
+ hasNextPage
19
+ endCursor
20
+ }
21
+ totalCount
22
+ }
23
+ }
24
+ """
25
+
26
+ GET_CLIENT = """
27
+ query GetClient($id: ID!) {
28
+ client(id: $id) {
29
+ id
30
+ firstName
31
+ lastName
32
+ companyName
33
+ email
34
+ phone
35
+ phones { nodes { number smsAllowed } }
36
+ billingAddress {
37
+ street1
38
+ street2
39
+ city
40
+ province
41
+ postalCode
42
+ country
43
+ }
44
+ tags
45
+ createdAt
46
+ updatedAt
47
+ }
48
+ }
49
+ """
50
+
51
+ SEARCH_CLIENTS = """
52
+ query SearchClients($query: String!, $first: Int) {
53
+ clients(first: $first, filter: {search: $query}) {
54
+ nodes {
55
+ id
56
+ firstName
57
+ lastName
58
+ companyName
59
+ email
60
+ phone
61
+ }
62
+ totalCount
63
+ }
64
+ }
65
+ """
66
+
67
+ # Job Queries
68
+ LIST_JOBS = """
69
+ query ListJobs($first: Int, $after: String, $status: String) {
70
+ jobs(first: $first, after: $after, filter: {status: $status}) {
71
+ nodes {
72
+ id
73
+ title
74
+ jobNumber
75
+ status
76
+ client {
77
+ id
78
+ firstName
79
+ lastName
80
+ companyName
81
+ }
82
+ startAt
83
+ endAt
84
+ createdAt
85
+ updatedAt
86
+ }
87
+ pageInfo {
88
+ hasNextPage
89
+ endCursor
90
+ }
91
+ totalCount
92
+ }
93
+ }
94
+ """
95
+
96
+ GET_JOB = """
97
+ query GetJob($id: ID!) {
98
+ job(id: $id) {
99
+ id
100
+ title
101
+ jobNumber
102
+ status
103
+ description
104
+ client {
105
+ id
106
+ firstName
107
+ lastName
108
+ companyName
109
+ email
110
+ }
111
+ property {
112
+ id
113
+ address {
114
+ street1
115
+ street2
116
+ city
117
+ province
118
+ postalCode
119
+ }
120
+ }
121
+ startAt
122
+ endAt
123
+ totalAmount
124
+ createdAt
125
+ updatedAt
126
+ }
127
+ }
128
+ """
129
+
130
+ # Quote Queries
131
+ LIST_QUOTES = """
132
+ query ListQuotes($first: Int, $after: String, $status: String) {
133
+ quotes(first: $first, after: $after, filter: {status: $status}) {
134
+ nodes {
135
+ id
136
+ quoteNumber
137
+ title
138
+ status
139
+ client {
140
+ id
141
+ firstName
142
+ lastName
143
+ companyName
144
+ }
145
+ totalAmount
146
+ sentAt
147
+ createdAt
148
+ updatedAt
149
+ }
150
+ pageInfo {
151
+ hasNextPage
152
+ endCursor
153
+ }
154
+ totalCount
155
+ }
156
+ }
157
+ """
158
+
159
+ GET_QUOTE = """
160
+ query GetQuote($id: ID!) {
161
+ quote(id: $id) {
162
+ id
163
+ quoteNumber
164
+ title
165
+ status
166
+ message
167
+ client {
168
+ id
169
+ firstName
170
+ lastName
171
+ companyName
172
+ email
173
+ }
174
+ lineItems {
175
+ id
176
+ name
177
+ description
178
+ quantity
179
+ unitPrice
180
+ total
181
+ }
182
+ subtotal
183
+ taxAmount
184
+ totalAmount
185
+ sentAt
186
+ createdAt
187
+ updatedAt
188
+ }
189
+ }
190
+ """
191
+
192
+ # Invoice Queries
193
+ LIST_INVOICES = """
194
+ query ListInvoices($first: Int, $after: String, $status: String) {
195
+ invoices(first: $first, after: $after, filter: {status: $status}) {
196
+ nodes {
197
+ id
198
+ invoiceNumber
199
+ subject
200
+ status
201
+ client {
202
+ id
203
+ firstName
204
+ lastName
205
+ companyName
206
+ }
207
+ totalAmount
208
+ amountPaid
209
+ balance
210
+ dueDate
211
+ sentAt
212
+ createdAt
213
+ updatedAt
214
+ }
215
+ pageInfo {
216
+ hasNextPage
217
+ endCursor
218
+ }
219
+ totalCount
220
+ }
221
+ }
222
+ """
223
+
224
+ GET_INVOICE = """
225
+ query GetInvoice($id: ID!) {
226
+ invoice(id: $id) {
227
+ id
228
+ invoiceNumber
229
+ subject
230
+ status
231
+ message
232
+ client {
233
+ id
234
+ firstName
235
+ lastName
236
+ companyName
237
+ email
238
+ }
239
+ job {
240
+ id
241
+ title
242
+ jobNumber
243
+ }
244
+ lineItems {
245
+ id
246
+ name
247
+ description
248
+ quantity
249
+ unitPrice
250
+ total
251
+ }
252
+ subtotal
253
+ taxAmount
254
+ totalAmount
255
+ amountPaid
256
+ balance
257
+ dueDate
258
+ sentAt
259
+ paidAt
260
+ createdAt
261
+ updatedAt
262
+ }
263
+ }
264
+ """
File without changes