mphapi 0.1.0__tar.gz

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.
mphapi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.1
2
+ Name: mphapi
3
+ Version: 0.1.0
4
+ Summary: A Python interface to the MyPriceHealth API
5
+ Author: David Archibald
6
+ Author-email: davidarchibald@myprice.health
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: pydantic (>=2.6.4,<3.0.0)
13
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
@@ -0,0 +1,5 @@
1
+ from .claim import * # noqa: F403, F401
2
+ from .client import * # noqa: F403, F401
3
+ from .date import * # noqa: F403, F401
4
+ from .pricing import * # noqa: F403, F401
5
+ from .response import * # noqa: F403, F401
@@ -0,0 +1,332 @@
1
+ from enum import Enum, IntEnum
2
+ from typing import Optional
3
+
4
+ from pydantic import AliasGenerator, BaseModel, ConfigDict, Field
5
+ from pydantic.alias_generators import to_camel
6
+
7
+ from .date import Date
8
+
9
+ camel_case_model_config = ConfigDict(
10
+ alias_generator=AliasGenerator(
11
+ validation_alias=to_camel, serialization_alias=to_camel
12
+ ),
13
+ populate_by_name=True,
14
+ )
15
+
16
+
17
+ class FormType(str, Enum):
18
+ """Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
19
+
20
+ HCFA = "HCFA"
21
+ UB_04 = "UB-04"
22
+
23
+
24
+ class BillTypeSequence(str, Enum):
25
+ """Where the claim is at in its billing lifecycle (e.g. 0: Non-Pay, 1: Admit Through
26
+ Discharge, 7: Replacement, etc.) (from CLM05_03)
27
+ """
28
+
29
+ NON_PAY = "G"
30
+ ADMIT_THROUGH_DISCHARGE = "H"
31
+ FIRST_INTERIM = "I"
32
+ CONTINUING_INTERIM = "J"
33
+ LAST_INTERIM = "K"
34
+ LATE_CHARGE = "M"
35
+ FIRST_INTERIM_DEPRECATED = "P"
36
+ REPLACEMENT = "Q"
37
+ VOID_OR_CANCEL = "0"
38
+ FINAL_CLAIM = "1"
39
+ CWF_ADJUSTMENT = "2"
40
+ CMS_ADJUSTMENT = "3"
41
+ INTERMEDIARY_ADJUSTMENT = "4"
42
+ OTHER_ADJUSTMENT = "5"
43
+ OIG_ADJUSTMENT = "6"
44
+ MSP_ADJUSTMENT = "7"
45
+ QIO_ADJUSTMENT = "8"
46
+ PROVIDER_ADJUSTMENT = "9"
47
+
48
+
49
+ class SexType(IntEnum):
50
+ """Biological sex of the patient for clinical purposes"""
51
+
52
+ UNKNOWN = 0
53
+ MALE = 1
54
+ FEMALE = 2
55
+
56
+
57
+ class Provider(BaseModel):
58
+ model_config = camel_case_model_config
59
+
60
+ npi: str
61
+ """National Provider Identifier of the provider (from NM109, required)"""
62
+
63
+ provider_tax_id: Optional[str] = None
64
+ """City of the provider (from N401, highly recommended)"""
65
+
66
+ provider_phones: Optional[list[str]] = None
67
+ """Address line 1 of the provider (from N301, highly recommended)"""
68
+
69
+ provider_faxes: Optional[list[str]] = None
70
+ """Commercial number of the provider used by some payers (from REF G2, optional)"""
71
+
72
+ provider_emails: Optional[list[str]] = None
73
+ """State license number of the provider (from REF 0B, optional)"""
74
+
75
+ provider_license_number: Optional[str] = None
76
+ """Last name of the provider (from NM103, highly recommended)"""
77
+
78
+ provider_commercial_number: Optional[str] = None
79
+ """Email addresses of the provider (from PER, optional)"""
80
+
81
+ provider_taxonomy: Optional[str] = None
82
+ """State of the provider (from N402, highly recommended)"""
83
+
84
+ provider_first_name: Optional[str] = None
85
+ """Taxonomy code of the provider (from PRV03, highly recommended)"""
86
+
87
+ provider_last_name: Optional[str] = None
88
+ """First name of the provider (NM104, highly recommended)"""
89
+
90
+ provider_org_name: Optional[str] = None
91
+ """Organization name of the provider (from NM103, highly recommended)"""
92
+
93
+ provider_address1: Optional[str] = None
94
+ """Tax ID of the provider (from REF highly recommended)"""
95
+
96
+ provider_address2: Optional[str] = None
97
+ """Phone numbers of the provider (from PER, optional)"""
98
+
99
+ provider_city: Optional[str] = None
100
+ """Fax numbers of the provider (from PER, optional)"""
101
+
102
+ provider_state: Optional[str] = None
103
+ """Address line 2 of the provider (from N302, optional)"""
104
+
105
+ provider_zip: str
106
+ """ZIP code of the provider (from N403, required)"""
107
+
108
+
109
+ class ValueCode(BaseModel):
110
+ """Code indicating the type of value provided (from HIxx_02)"""
111
+
112
+ model_config = camel_case_model_config
113
+
114
+ code: str
115
+
116
+ """Amount associated with the value code (from HIxx_05)"""
117
+ amount: float
118
+
119
+
120
+ class Diagnosis(BaseModel):
121
+ """Principal ICD diagnosis for the patient (from HI ABK or BK)"""
122
+
123
+ model_config = camel_case_model_config
124
+
125
+ code: str
126
+ """ICD code for the diagnosis"""
127
+
128
+ description: Optional[str] = None
129
+ """Description of the diagnosis"""
130
+
131
+
132
+ class Service(BaseModel):
133
+ model_config = camel_case_model_config
134
+
135
+ provider: Optional[Provider] = None
136
+ """Additional provider information specific to this service item"""
137
+
138
+ line_number: Optional[str] = None
139
+ """Unique line number for the service item (from LX01)"""
140
+
141
+ rev_code: Optional[str] = None
142
+ """Revenue code (from SV2_01)"""
143
+
144
+ procedure_code: Optional[str] = None
145
+ """Procedure code (from SV101_02 / SV202_02)"""
146
+
147
+ procedure_modifiers: Optional[str] = None
148
+ """Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
149
+
150
+ drug_code: Optional[str] = None
151
+ """National Drug Code (from LIN03)"""
152
+
153
+ date_from: Optional[Date] = None
154
+ """Begin date of service (from DTP 472)"""
155
+
156
+ date_through: Optional[Date] = None
157
+ """End date of service (from DTP 472)"""
158
+
159
+ billed_amount: Optional[float] = None
160
+ """Billed charge for the service (from SV102 / SV203)"""
161
+
162
+ allowed_amount: Optional[float] = None
163
+ """Plan allowed amount for the service (non-EDI)"""
164
+
165
+ paid_amount: Optional[float] = None
166
+ """Plan paid amount for the service (non-EDI)"""
167
+
168
+ quantity: Optional[float] = None
169
+ """Quantity of the service (from SV104 / SV205)"""
170
+
171
+ units: Optional[str] = None
172
+ """Units connected to the quantity given (from SV103 / SV204)"""
173
+
174
+ place_of_service: Optional[str] = None
175
+ """Place of service code (from SV105)"""
176
+
177
+ diagnosis_pointers: Optional[list[int]] = None
178
+ """Diagnosis pointers (from SV107)"""
179
+
180
+ ambulance_pickup_zip: Optional[str] = None
181
+ """ZIP code where ambulance picked up patient. Supplied if different than claim-level value (from NM1 PW)"""
182
+
183
+
184
+ class Claim(Provider, BaseModel):
185
+ model_config = camel_case_model_config
186
+
187
+ claim_id: Optional[str] = None
188
+ """Unique identifier for the claim (from REF D9)"""
189
+
190
+ plan_code: Optional[str] = None
191
+ """Identifies the subscriber's plan (from SBR03)"""
192
+
193
+ patient_sex: Optional[SexType] = None
194
+ """Biological sex of the patient for clinical purposes (from DMG02). 0:Unknown, 1:Male,
195
+ 2:Female
196
+ """
197
+
198
+ patient_date_of_birth: Optional[Date] = None
199
+ """Patient date of birth (from DMG03)"""
200
+
201
+ patient_height_in_cm: Optional[float] = None
202
+ """Patient height in centimeters (from HI value A9, MEA value HT)"""
203
+
204
+ patient_weight_in_kg: Optional[float] = None
205
+ """Patient weight in kilograms (from HI value A8, PAT08, CR102 [ambulance only])"""
206
+
207
+ ambulance_pickup_zip: Optional[str] = None
208
+ """Location where patient was picked up in ambulance (from HI with HIxx_01=BE and HIxx_02=A0
209
+ or NM1 loop with NM1 PW)
210
+ """
211
+
212
+ form_type: Optional[FormType] = None
213
+ """Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
214
+
215
+ bill_type_or_pos: Optional[str] = None
216
+ """Describes type of facility where services were rendered (from CLM05_01)"""
217
+
218
+ bill_type_sequence: Optional[BillTypeSequence] = None
219
+ """Where the claim is at in its billing lifecycle (e.g. 0: Non-Pay, 1: Admit Through
220
+ Discharge, 7: Replacement, etc.) (from CLM05_03)
221
+ """
222
+
223
+ billed_amount: Optional[float] = None
224
+ """Billed amount from provider (from CLM02)"""
225
+
226
+ allowed_amount: Optional[float] = None
227
+ """Amount allowed by the plan for payment. Both member and plan responsibility (non-EDI)"""
228
+
229
+ paid_amount: Optional[float] = None
230
+ """Amount paid by the plan for the claim (non-EDI)"""
231
+
232
+ date_from: Optional[Date] = None
233
+ """Earliest service date among services, or statement date if not found"""
234
+
235
+ date_through: Optional[Date] = None
236
+ """Latest service date among services, or statement date if not found"""
237
+
238
+ discharge_status: Optional[str] = None
239
+ """Status of the patient at time of discharge (from CL103)"""
240
+
241
+ admit_diagnosis: Optional[str] = None
242
+ """ICD diagnosis at the time the patient was admitted (from HI ABJ or BJ)"""
243
+
244
+ principal_diagnosis: Optional[Diagnosis] = None
245
+ """Principal ICD diagnosis for the patient (from HI ABK or BK)"""
246
+
247
+ other_diagnoses: Optional[list[Diagnosis]] = None
248
+ """Other ICD diagnoses that apply to the patient (from HI ABF or BF)"""
249
+
250
+ principal_procedure: Optional[str] = None
251
+ """Principal ICD procedure for the patient (from HI BBR or BR)"""
252
+
253
+ other_procedures: Optional[list[str]] = None
254
+ """Other ICD procedures that apply to the patient (from HI BBQ or BQ)"""
255
+
256
+ condition_codes: Optional[list[str]] = None
257
+ """Special conditions that may affect payment or other processing (from HI BG)"""
258
+
259
+ value_codes: Optional[list[ValueCode]] = None
260
+ """Numeric values related to the patient or claim (HI BE)"""
261
+
262
+ occurrence_codes: Optional[list[str]] = None
263
+ """Date related occurrences related to the patient or claim (from HI BH)"""
264
+
265
+ drg: Optional[str] = None
266
+ """Diagnosis Related Group for inpatient services (from HI DR)"""
267
+
268
+ services: list[Service] = Field(min_length=1)
269
+ """One or more services provided to the patient (from LX loop)"""
270
+
271
+
272
+ class RateSheetService(BaseModel):
273
+ model_config = camel_case_model_config
274
+
275
+ procedure_code: str
276
+ """Procedure code (from SV101_02 / SV202_02)"""
277
+
278
+ procedure_modifiers: list[str]
279
+ """Procedure modifiers (from SV101_03, 4, 5, 6 / SV202_03, 4, 5, 6)"""
280
+
281
+ billed_amount: float
282
+ """Billed charge for the service (from SV102 / SV203)"""
283
+
284
+ allowed_amount: float
285
+ """Plan allowed amount for the service (non-EDI)"""
286
+
287
+
288
+ class RateSheet(BaseModel):
289
+ npi: str
290
+ """National Provider Identifier of the provider (from NM109, required)"""
291
+
292
+ provider_first_name: str
293
+ """First name of the provider (NM104, highly recommended)"""
294
+
295
+ provider_last_name: str
296
+ """Last name of the provider (from NM103, highly recommended)"""
297
+
298
+ provider_org_name: str
299
+ """Organization name of the provider (from NM103, highly recommended)"""
300
+
301
+ provider_address: str
302
+ """Address of the provider (from N301, highly recommended)"""
303
+
304
+ provider_city: str
305
+ """City of the provider (from N401, highly recommended)"""
306
+
307
+ provider_state: str
308
+ """State of the provider (from N402, highly recommended)"""
309
+
310
+ provider_zip: str
311
+ """ZIP code of the provider (from N403, required)"""
312
+
313
+ form_type: FormType
314
+ """Type of form used to submit the claim. Can be HCFA or UB-04 (from CLM05_02)"""
315
+
316
+ bill_type_or_pos: str
317
+ """Describes type of facility where services were rendered (from CLM05_01)"""
318
+
319
+ drg: str
320
+ """Diagnosis Related Group for inpatient services (from HI DR)"""
321
+
322
+ billed_amount: float
323
+ """Billed amount from provider (from CLM02)"""
324
+
325
+ allowed_amount: float
326
+ """Amount allowed by the plan for payment. Both member and plan responsibility (non-EDI)"""
327
+
328
+ paid_amount: float
329
+ """Amount paid by the plan for the claim (non-EDI)"""
330
+
331
+ services: list[RateSheetService]
332
+ """One or more services provided to the patient (from LX loop)"""
@@ -0,0 +1,215 @@
1
+ import urllib.parse
2
+ from typing import Any, Mapping, Sequence
3
+
4
+ import requests
5
+ from pydantic import BaseModel, StrictBool, TypeAdapter
6
+
7
+ from .claim import Claim, RateSheet
8
+ from .pricing import Pricing
9
+ from .response import Response, Responses
10
+
11
+ Header = Mapping[str, str | bytes | None]
12
+
13
+
14
+ class PriceConfig(BaseModel):
15
+ """PriceConfig is used to configure the behavior of the pricing API"""
16
+
17
+ is_commercial: StrictBool
18
+ """set to true to use commercial code crosswalks"""
19
+
20
+ disable_cost_based_reimbursement: StrictBool
21
+ """by default, the API will use cost-based reimbursement for MAC priced line-items. This is the best estimate we have for this proprietary pricing"""
22
+
23
+ use_commercial_synthetic_for_not_allowed: StrictBool
24
+ """set to true to use a synthetic Medicare price for line-items that are not allowed by Medicare"""
25
+
26
+ use_drg_from_grouper: StrictBool
27
+ """set to true to always use the DRG from the inpatient grouper"""
28
+
29
+ use_best_drg_price: StrictBool
30
+ """set to true to use the best DRG price between the price on the claim and the price from the grouper"""
31
+
32
+ override_threshold: float
33
+ """set to a value greater than 0 to allow the pricer flexibility to override NCCI edits and other overridable errors and return a price"""
34
+
35
+ include_edits: StrictBool
36
+ """set to true to include edit details in the response"""
37
+
38
+
39
+ class Client:
40
+ url: str
41
+ headers: Header
42
+
43
+ def __init__(self, apiKey: str, isTest: bool = False):
44
+ if isTest:
45
+ self.url = "https://api-test.myprice.health"
46
+ else:
47
+ self.url = "https://api.myprice.health"
48
+
49
+ self.headers = {"x-api-key": apiKey}
50
+
51
+ def _do_request(
52
+ self,
53
+ path: str,
54
+ json: Any | None,
55
+ method: str = "POST",
56
+ headers: Header = {},
57
+ ) -> requests.Response:
58
+ return requests.request(
59
+ method,
60
+ urllib.parse.urljoin(self.url, path),
61
+ json=json,
62
+ headers={**self.headers, **headers},
63
+ )
64
+
65
+ def _receive_response[
66
+ Model: BaseModel
67
+ ](
68
+ self,
69
+ path: str,
70
+ body: BaseModel,
71
+ response_model: type[Model],
72
+ method: str = "POST",
73
+ headers: Header = {},
74
+ ) -> Model:
75
+ """
76
+ Raises:
77
+ ValueError
78
+ When response cannot be decoded.
79
+ mphapi.APIError
80
+ The error returned when the api returns an error.
81
+ """
82
+
83
+ response = self._do_request(
84
+ path,
85
+ body.model_dump(mode="json", by_alias=True, exclude_none=True),
86
+ method,
87
+ headers,
88
+ )
89
+
90
+ return (
91
+ Response[response_model]
92
+ .model_validate_json(response.content, strict=True)
93
+ .result()
94
+ )
95
+
96
+ def _receive_responses[
97
+ Model: BaseModel
98
+ ](
99
+ self,
100
+ path: str,
101
+ body: Sequence[BaseModel],
102
+ response_model: type[Model],
103
+ method: str = "POST",
104
+ headers: Header = {},
105
+ ) -> list[Model]:
106
+ """
107
+ Raises:
108
+ ValueError
109
+ When response cannot be decoded.
110
+ mphapi.APIError
111
+ The error returned when the api returns an error.
112
+ """
113
+
114
+ response = self._do_request(
115
+ path,
116
+ TypeAdapter(type(body)).dump_python(
117
+ body, mode="json", by_alias=True, exclude_none=True
118
+ ),
119
+ method,
120
+ headers,
121
+ )
122
+
123
+ return (
124
+ Responses[response_model]
125
+ .model_validate_json(response.content, strict=True)
126
+ .results()
127
+ )
128
+
129
+ def estimate_rate_sheet(self, *inputs: RateSheet) -> list[Pricing]:
130
+ """
131
+ Raises:
132
+ ValueError
133
+ When response cannot be decoded.
134
+ mphapi.APIError
135
+ The error returned when the api returns an error.
136
+ """
137
+
138
+ return self._receive_responses(
139
+ "/v1/medicare/estimate/rate-sheet",
140
+ inputs,
141
+ Pricing,
142
+ )
143
+
144
+ def estimate_claims(self, config: PriceConfig, *inputs: Claim) -> list[Pricing]:
145
+ """
146
+ Raises:
147
+ ValueError
148
+ When response cannot be decoded.
149
+ mphapi.APIError
150
+ The error returned when the api returns an error.
151
+ """
152
+
153
+ return self._receive_responses(
154
+ "/v1/medicare/estimate/claims",
155
+ inputs,
156
+ Pricing,
157
+ headers=self._get_price_headers(config),
158
+ )
159
+
160
+ def price(self, config: PriceConfig, input: Claim) -> Pricing:
161
+ """
162
+ Raises:
163
+ ValueError
164
+ When response cannot be decoded.
165
+ mphapi.APIError
166
+ The error returned when the api returns an error.
167
+ """
168
+
169
+ return self._receive_response(
170
+ "/v1/medicare/price/claim",
171
+ input,
172
+ Pricing,
173
+ headers=self._get_price_headers(config),
174
+ )
175
+
176
+ def price_batch(self, config: PriceConfig, *input: Claim) -> list[Pricing]:
177
+ """
178
+ Raises:
179
+ ValueError
180
+ When response cannot be decoded.
181
+ mphapi.APIError
182
+ The error returned when the api returns an error.
183
+ """
184
+
185
+ return self._receive_responses(
186
+ "/v1/medicare/price/claims",
187
+ input,
188
+ Pricing,
189
+ headers=self._get_price_headers(config),
190
+ )
191
+
192
+ def _get_price_headers(self, config: PriceConfig) -> Header:
193
+ headers: Header = {}
194
+ if config.is_commercial:
195
+ headers["is-commercial"] = "true"
196
+
197
+ if config.disable_cost_based_reimbursement:
198
+ headers["disable-cost-based-reimbursement"] = "true"
199
+
200
+ if config.use_commercial_synthetic_for_not_allowed:
201
+ headers["use-commercial-synthetic-for-not-allowed"] = "true"
202
+
203
+ if config.override_threshold > 0:
204
+ headers["override-threshold"] = str(config.override_threshold)
205
+
206
+ if config.include_edits:
207
+ headers["include-edits"] = "true"
208
+
209
+ if config.use_drg_from_grouper:
210
+ headers["use-drg-from-grouper"] = "true"
211
+
212
+ if config.use_best_drg_price:
213
+ headers["use-best-drg-price"] = "true"
214
+
215
+ return headers
@@ -0,0 +1,51 @@
1
+ from datetime import datetime
2
+ from typing import Any
3
+
4
+ from pydantic import GetCoreSchemaHandler
5
+ from pydantic_core import CoreSchema, core_schema
6
+
7
+
8
+ class Date:
9
+ """Date is a custom type for representing dates in the format YYYYMMDD"""
10
+
11
+ year: int
12
+ month: int
13
+ day: int
14
+
15
+ def __init__(self, year: int, month: int, day: int):
16
+ self.year = year
17
+ self.month = month
18
+ self.day = day
19
+
20
+ def __str__(self):
21
+ return f"{self.year}{self.month:02d}{self.day:02d}"
22
+
23
+ # Based off of this: https://docs.pydantic.dev/2.1/usage/types/custom/#handling-third-party-types
24
+ @classmethod
25
+ def __get_pydantic_core_schema__(
26
+ cls, source_type: Any, handler: GetCoreSchemaHandler
27
+ ) -> CoreSchema:
28
+ def to_date(value: str) -> Date:
29
+ time = datetime.strptime(value, "%Y%m%d")
30
+
31
+ return Date(time.year, time.month, time.day)
32
+
33
+ from_str = core_schema.chain_schema(
34
+ [
35
+ core_schema.str_schema(),
36
+ core_schema.no_info_plain_validator_function(to_date),
37
+ ]
38
+ )
39
+
40
+ return core_schema.json_or_python_schema(
41
+ json_schema=from_str,
42
+ python_schema=core_schema.union_schema(
43
+ [
44
+ core_schema.is_instance_schema(Date),
45
+ from_str,
46
+ ]
47
+ ),
48
+ serialization=core_schema.plain_serializer_function_ser_schema(
49
+ lambda date: str(date)
50
+ ),
51
+ )
@@ -0,0 +1,337 @@
1
+ from enum import Enum
2
+ from typing import Any, Optional
3
+
4
+ from pydantic import BaseModel, Field, GetCoreSchemaHandler
5
+ from pydantic_core import core_schema
6
+
7
+ from .claim import Service, camel_case_model_config
8
+ from .response import ResponseError
9
+
10
+
11
+ class ClaimRepricingCode(str, Enum):
12
+ """claim-level repricing codes"""
13
+
14
+ MEDICARE = "MED"
15
+ CONTRACT_PRICING = "CON"
16
+ RBP_PRICING = "RBP"
17
+ SINGLE_CASE_AGREEMENT = "SCA"
18
+ NEEDS_MORE_INFO = "IFO"
19
+
20
+
21
+ class LineRepricingCode(str, Enum):
22
+ # line-level Medicare repricing codes
23
+ MEDICARE = "MED"
24
+ SYNTHETIC_MEDICARE = "SYN"
25
+ COST_PERCENT = "CST"
26
+ MEDICARE_PERCENT = "MPT"
27
+ MEDICARE_NO_OUTLIER = "MNO"
28
+ BILLED_PERCENT = "BIL"
29
+ FEE_SCHEDULE = "FSC"
30
+ PER_DIEM = "PDM"
31
+ FLAT_RATE = "FLT"
32
+ LIMITED_TO_BILLED = "LTB"
33
+
34
+ # line-level zero dollar repricing explanations
35
+ NOT_ALLOWED_BY_MEDICARE = "NAM"
36
+ PACKAGED = "PKG"
37
+ NEEDS_MORE_INFO = "IFO"
38
+ PROCEDURE_CODE_PROBLEM = "CPB"
39
+ NOT_REPRICED_PER_REQUEST = "NRP"
40
+
41
+
42
+ class HospitalType(str, Enum):
43
+ ACUTE_CARE = "Acute Care Hospitals"
44
+ CRITICAL_ACCESS = "Critical Access Hospitals"
45
+ CHILDRENS = "Childrens"
46
+ PSYCHIATRIC = "Psychiatric"
47
+ ACUTE_CARE_DOD = "Acute Care - Department of Defense"
48
+
49
+
50
+ class InpatientPriceDetail(BaseModel):
51
+ """InpatientPriceDetail contains pricing details for an inpatient claim"""
52
+
53
+ model_config = camel_case_model_config
54
+
55
+ drg: Optional[str] = None
56
+ """Diagnosis Related Group (DRG) code used to price the claim"""
57
+
58
+ drg_amount: Optional[float] = None
59
+ """Amount Medicare would pay for the DRG"""
60
+
61
+ passthrough_amount: Optional[float] = None
62
+ """Per diem amount to cover capital-related costs, direct medical education, and other costs"""
63
+
64
+ outlier_amount: Optional[float] = None
65
+ """Additional amount paid for high cost cases"""
66
+
67
+ indirect_medical_education_amount: Optional[float] = None
68
+ """Additional amount paid for teaching hospitals"""
69
+
70
+ disproportionate_share_amount: Optional[float] = None
71
+ """Additional amount paid for hospitals with a high number of low-income patients"""
72
+
73
+ uncompensated_care_amount: Optional[float] = None
74
+ """Additional amount paid for patients who are unable to pay for their care"""
75
+
76
+ readmission_adjustment_amount: Optional[float] = None
77
+ """Adjustment amount for hospitals with high readmission rates"""
78
+
79
+ value_based_purchasing_amount: Optional[float] = None
80
+ """Adjustment for hospitals based on quality measures"""
81
+
82
+
83
+ class OutpatientPriceDetail(BaseModel):
84
+ """OutpatientPriceDetail contains pricing details for an outpatient claim"""
85
+
86
+ model_config = camel_case_model_config
87
+
88
+ outlier_amount: float
89
+ """Additional amount paid for high cost cases"""
90
+
91
+ first_passthrough_drug_offset_amount: float
92
+ """Amount built into the APC payment for certain drugs"""
93
+
94
+ second_passthrough_drug_offset_amount: float
95
+ """Amount built into the APC payment for certain drugs"""
96
+
97
+ third_passthrough_drug_offset_amount: float
98
+ """Amount built into the APC payment for certain drugs"""
99
+
100
+ first_device_offset_amount: float
101
+ """Amount built into the APC payment for certain devices"""
102
+
103
+ second_device_offset_amount: float
104
+ """Amount built into the APC payment for certain devices"""
105
+
106
+ full_or_partial_device_credit_offset_amount: float
107
+ """Credit for devices that are supplied for free or at a reduced cost"""
108
+
109
+ terminated_device_procedure_offset_amount: float
110
+ """Credit for devices that are not used due to a terminated procedure"""
111
+
112
+
113
+ class RuralIndicator(str, Enum):
114
+ RURAL = "R"
115
+ SUPER_RURAL = "B"
116
+ URBAN = ""
117
+
118
+ @classmethod
119
+ def __get_pydantic_core_schema__(
120
+ cls,
121
+ _source_type: Any,
122
+ _handler: GetCoreSchemaHandler,
123
+ ) -> core_schema.CoreSchema:
124
+ def from_int(value: int) -> RuralIndicator:
125
+ if value == 82:
126
+ return RuralIndicator.RURAL
127
+ elif value == 66:
128
+ return RuralIndicator.SUPER_RURAL
129
+ elif value == 32:
130
+ return RuralIndicator.URBAN
131
+ else:
132
+ raise ValueError(f"Unknown rural indicator value: {value}")
133
+
134
+ def to_int(instance: RuralIndicator) -> int:
135
+ if instance == RuralIndicator.RURAL:
136
+ return 82
137
+ elif instance == RuralIndicator.SUPER_RURAL:
138
+ return 66
139
+ elif instance == RuralIndicator.URBAN:
140
+ return 32
141
+ else:
142
+ raise ValueError(f"Unknown rural indicator: {instance}")
143
+
144
+ from_int_schema = core_schema.chain_schema(
145
+ [
146
+ core_schema.int_schema(),
147
+ core_schema.no_info_plain_validator_function(from_int),
148
+ ]
149
+ )
150
+
151
+ return core_schema.json_or_python_schema(
152
+ json_schema=from_int_schema,
153
+ python_schema=core_schema.union_schema(
154
+ [
155
+ # check if it's an instance first before doing any further work
156
+ core_schema.is_instance_schema(RuralIndicator),
157
+ from_int_schema,
158
+ ]
159
+ ),
160
+ serialization=core_schema.plain_serializer_function_ser_schema(to_int),
161
+ )
162
+
163
+
164
+ class ProviderDetail(BaseModel):
165
+ """
166
+ ProviderDetail contains basic information about the provider and/or locality used for pricing.
167
+ Not all fields are returned with every pricing request. For example, the CMS Certification
168
+ Number (CCN) is only returned for facilities which have a CCN such as hospitals.
169
+ """
170
+
171
+ model_config = camel_case_model_config
172
+
173
+ ccn: Optional[str] = None
174
+ """CMS Certification Number for the facility"""
175
+
176
+ mac: Optional[int] = None
177
+ """Medicare Administrative Contractor number"""
178
+
179
+ locality: Optional[int] = None
180
+ """Geographic locality number used for pricing"""
181
+
182
+ rural_indicator: Optional[RuralIndicator] = None
183
+ """Indicates whether provider is Rural (R), Super Rural (B), or Urban (blank)"""
184
+
185
+ specialty_type: Optional[str] = None
186
+ """Medicare provider specialty type"""
187
+
188
+ hospital_type: Optional[HospitalType] = None
189
+ """Type of hospital"""
190
+
191
+
192
+ class ClaimEdits(BaseModel):
193
+ """ClaimEdits contains errors which cause the claim to be denied, rejected, suspended, or returned to the provider."""
194
+
195
+ model_config = camel_case_model_config
196
+
197
+ claim_overall_disposition: Optional[str] = None
198
+ claim_rejection_disposition: Optional[str] = None
199
+ claim_denial_disposition: Optional[str] = None
200
+ claim_return_to_provider_disposition: Optional[str] = None
201
+ claim_suspension_disposition: Optional[str] = None
202
+ line_item_rejection_disposition: Optional[str] = None
203
+ line_item_denial_disposition: Optional[str] = None
204
+ claim_rejection_reasons: Optional[list[str]] = None
205
+ claim_denial_reasons: Optional[list[str]] = None
206
+ claim_return_to_provider_reasons: Optional[list[str]] = None
207
+ claim_suspension_reasons: Optional[list[str]] = None
208
+ line_item_rejection_reasons: Optional[list[str]] = None
209
+ line_item_denial_reasons: Optional[list[str]] = None
210
+
211
+
212
+ class Pricing(BaseModel):
213
+ """Pricing contains the results of a pricing request"""
214
+
215
+ model_config = camel_case_model_config
216
+
217
+ claim_id: Optional[str] = None
218
+ """The unique identifier for the claim (copied from input)"""
219
+
220
+ medicare_amount: Optional[float] = None
221
+ """The amount Medicare would pay for the service"""
222
+
223
+ allowed_amount: Optional[float] = None
224
+ """The allowed amount based on a contract or RBP pricing"""
225
+
226
+ allowed_calculation_error: Optional[str] = None
227
+ """The reason the allowed amount was not calculated"""
228
+
229
+ medicare_repricing_code: Optional[ClaimRepricingCode] = None
230
+ """Explains the methodology used to calculate Medicare (MED or IFO)"""
231
+
232
+ medicare_repricing_note: Optional[str] = None
233
+ """Note explaining approach for pricing or reason for error"""
234
+
235
+ allowed_repricing_code: Optional[ClaimRepricingCode] = None
236
+ """Explains the methodology used to calculate allowed amount (CON, RBP, SCA, or IFO)"""
237
+
238
+ allowed_repricing_note: Optional[str] = None
239
+ """Note explaining approach for pricing or reason for error"""
240
+
241
+ medicare_std_dev: Optional[float] = None
242
+ """The standard deviation of the estimated Medicare amount (estimates service only)"""
243
+
244
+ medicare_source: Optional[str] = None
245
+ """Source of the Medicare amount (e.g. physician fee schedule, OPPS, etc.)"""
246
+
247
+ inpatient_price_detail: Optional[InpatientPriceDetail] = None
248
+ """Details about the inpatient pricing"""
249
+
250
+ outpatient_price_detail: Optional[OutpatientPriceDetail] = None
251
+ """Details about the outpatient pricing"""
252
+
253
+ provider_detail: Optional[ProviderDetail] = None
254
+ """The provider details used when pricing the claim"""
255
+
256
+ edit_detail: Optional[ClaimEdits] = None
257
+ """Errors which cause the claim to be denied, rejected, suspended, or returned to the provider"""
258
+
259
+ pricer_result: Optional[str] = None
260
+ """Pricer return details"""
261
+
262
+ services: list[Service] = Field(min_length=1)
263
+ """Pricing for each service line on the claim"""
264
+
265
+ edit_error: Optional[ResponseError] = None
266
+ """An error that occurred during some step of the pricing process"""
267
+
268
+
269
+ class LineEdits(BaseModel):
270
+ """LineEdits contains errors which cause the line item to be unable to be priced."""
271
+
272
+ model_config = camel_case_model_config
273
+
274
+ denial_or_rejection_text: str
275
+ procedure_edits: list[str]
276
+ modifier1_edits: list[str]
277
+ modifier2_edits: list[str]
278
+ modifier3_edits: list[str]
279
+ modifier4_edits: list[str]
280
+ modifier5_edits: list[str]
281
+ data_edits: list[str]
282
+ revenue_edits: list[str]
283
+ professional_edits: list[str]
284
+
285
+
286
+ class PricedService(BaseModel):
287
+ """PricedService contains the results of a pricing request for a single service line"""
288
+
289
+ model_config = camel_case_model_config
290
+
291
+ line_number: str
292
+ """Number of the service line item (copied from input)"""
293
+
294
+ provider_detail: Optional[ProviderDetail] = None
295
+ """Provider Details used when pricing the service if different than the claim"""
296
+
297
+ medicare_amount: float
298
+ """Amount Medicare would pay for the service"""
299
+
300
+ allowed_amount: float
301
+ """Allowed amount based on a contract or RBP pricing"""
302
+
303
+ allowed_calculation_error: str
304
+ """Reason the allowed amount was not calculated"""
305
+
306
+ repricing_code: LineRepricingCode
307
+ """Explains the methodology used to calculate Medicare"""
308
+
309
+ repricing_note: str
310
+ """Note explaining approach for pricing or reason for error"""
311
+
312
+ technical_component_amount: float
313
+ """Amount Medicare would pay for the technical component"""
314
+
315
+ professional_component_amount: float
316
+ """Amount Medicare would pay for the professional component"""
317
+
318
+ medicare_std_dev: float
319
+ """Standard deviation of the estimated Medicare amount (estimates service only)"""
320
+
321
+ medicare_source: str
322
+ """Source of the Medicare amount (e.g. physician fee schedule, OPPS, etc.)"""
323
+
324
+ pricer_result: str
325
+ """Pricing service return details"""
326
+
327
+ status_indicator: str
328
+ """Code which gives more detail about how Medicare pays for the service"""
329
+
330
+ payment_indicator: str
331
+ """Text which explains the type of payment for Medicare"""
332
+
333
+ payment_apc: str
334
+ """Ambulatory Payment Classification"""
335
+
336
+ edit_detail: Optional[LineEdits] = None
337
+ """Errors which cause the line item to be unable to be priced"""
@@ -0,0 +1,124 @@
1
+ from pydantic import BaseModel, RootModel
2
+ from pydantic.dataclasses import dataclass
3
+
4
+
5
+ class APIError(Exception):
6
+ message: str
7
+
8
+ def __init__(self, message: str):
9
+ super().__init__(message)
10
+
11
+
12
+ @dataclass
13
+ class ResponseError(APIError):
14
+ title: str
15
+ detail: str
16
+
17
+ def __post_init__(self):
18
+ super().__init__(self.__str__())
19
+
20
+ def __str__(self) -> str:
21
+ return f"{self.title}: {self.detail}"
22
+
23
+
24
+ class ResponseSuccess[Result: BaseModel](BaseModel):
25
+ result: Result
26
+ status: int
27
+
28
+
29
+ class ResponseFailure(BaseModel):
30
+ error: ResponseError
31
+ status: int
32
+
33
+
34
+ @dataclass
35
+ class GatewayError(APIError):
36
+ message: str
37
+ code: int
38
+
39
+ def __post_init__(self):
40
+ super().__init__(self.message)
41
+
42
+
43
+ class Response[Result: BaseModel](
44
+ RootModel[ResponseSuccess[Result] | ResponseFailure | GatewayError]
45
+ ):
46
+ """
47
+ Response contains the standardized API response data used by all My Price Health API's. It is based off of the generalized error handling recommendation found
48
+ in IETF RFC 7807 https://tools.ietf.org/html/rfc7807 and is a simplification of the Spring Boot error response as described at https://www.baeldung.com/rest-api-error-handling-best-practices
49
+ """
50
+
51
+ """
52
+ An error response might look like this:
53
+ {
54
+ "error: {
55
+ "title": "Incorrect username or password.",
56
+ "detail": "Authentication failed due to incorrect username or password.",
57
+ }
58
+ "status": 401,
59
+ }
60
+
61
+ A successful response with a single result might look like this:
62
+ {
63
+ "result": {
64
+ "procedureCode": "ABC",
65
+ "billedAverage": 15.23
66
+ },
67
+ "status": 200,
68
+ }
69
+ """
70
+
71
+ def result(
72
+ self,
73
+ ) -> Result:
74
+ """Returns the result if it's successful, otherwise throws
75
+
76
+ Returns
77
+ -------
78
+ Result
79
+ The successful result.
80
+
81
+ Raises
82
+ ------
83
+ ResponseError
84
+ The request's error response.
85
+ """
86
+
87
+ if isinstance(self.root, ResponseSuccess):
88
+ return self.root.result
89
+ elif isinstance(self.root, ResponseFailure):
90
+ raise self.root.error
91
+ else:
92
+ raise self.root
93
+
94
+
95
+ class ResponsesSuccess[Result: BaseModel](BaseModel):
96
+ results: list[Result]
97
+ success_count: int
98
+ error_count: int
99
+ status_code: int
100
+
101
+
102
+ class Responses[Result: BaseModel](
103
+ RootModel[ResponsesSuccess[Result] | ResponseFailure]
104
+ ):
105
+ def results(
106
+ self,
107
+ ) -> list[Result]:
108
+ """Returns the result if it's successful, otherwise throws
109
+
110
+ Returns
111
+ -------
112
+ list[Result]
113
+ The results, although some may still have encountered errors.
114
+
115
+ Raises
116
+ ------
117
+ ResponseError
118
+ The request's error response.
119
+ """
120
+
121
+ if isinstance(self.root, ResponsesSuccess):
122
+ return self.root.results
123
+ else:
124
+ raise self.root.error
@@ -0,0 +1,15 @@
1
+ [tool.poetry]
2
+ name = "mphapi"
3
+ version = "0.1.0"
4
+ description = "A Python interface to the MyPriceHealth API"
5
+ authors = ["David Archibald <davidarchibald@myprice.health>"]
6
+
7
+ [tool.poetry.dependencies]
8
+ python = "^3.10"
9
+ pydantic = "^2.6.4"
10
+ requests = "^2.31.0"
11
+
12
+
13
+ [build-system]
14
+ requires = ["poetry-core"]
15
+ build-backend = "poetry.core.masonry.api"