ownerrez-wrapper 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.
- ownerrez_wrapper-0.1.0/LICENSE +21 -0
- ownerrez_wrapper-0.1.0/MANIFEST.in +5 -0
- ownerrez_wrapper-0.1.0/PKG-INFO +187 -0
- ownerrez_wrapper-0.1.0/README.md +147 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/__init__.py +15 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/api.py +84 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/cli.py +114 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/constants.py +1 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/exceptions.py +3 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/model.py +218 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper/restAdapter.py +76 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/PKG-INFO +187 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/SOURCES.txt +21 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/dependency_links.txt +1 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/entry_points.txt +2 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/requires.txt +12 -0
- ownerrez_wrapper-0.1.0/ownerrez_wrapper.egg-info/top_level.txt +2 -0
- ownerrez_wrapper-0.1.0/requirements-dev.txt +9 -0
- ownerrez_wrapper-0.1.0/requirements.txt +2 -0
- ownerrez_wrapper-0.1.0/setup.cfg +4 -0
- ownerrez_wrapper-0.1.0/setup.py +48 -0
- ownerrez_wrapper-0.1.0/tests/__init__.py +0 -0
- ownerrez_wrapper-0.1.0/tests/test_api.py +124 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Geody Moore
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,187 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: ownerrez-wrapper
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python wrapper for the OwnerRez API
|
5
|
+
Home-page: https://github.com/gmoorevt/ownerrez-wrapper
|
6
|
+
Author: Geody Moore
|
7
|
+
Author-email: geody.moore@gmail.com
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Developers
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Requires-Python: >=3.10
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
License-File: LICENSE
|
19
|
+
Requires-Dist: requests>=2.25.0
|
20
|
+
Requires-Dist: python-dateutil>=2.8.0
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
24
|
+
Requires-Dist: pytest-cov>=2.0.0; extra == "dev"
|
25
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
26
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
27
|
+
Requires-Dist: flake8>=3.9.0; extra == "dev"
|
28
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
29
|
+
Requires-Dist: freezegun>=1.2.0; extra == "dev"
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: description
|
34
|
+
Dynamic: description-content-type
|
35
|
+
Dynamic: home-page
|
36
|
+
Dynamic: provides-extra
|
37
|
+
Dynamic: requires-dist
|
38
|
+
Dynamic: requires-python
|
39
|
+
Dynamic: summary
|
40
|
+
|
41
|
+
# OwnerRez API Wrapper
|
42
|
+
|
43
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
- Python 3.10 or higher
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
```bash
|
52
|
+
pip install ownerrez-wrapper
|
53
|
+
```
|
54
|
+
|
55
|
+
## Python Usage
|
56
|
+
|
57
|
+
```python
|
58
|
+
from ownerrez_wrapper import API
|
59
|
+
from datetime import datetime
|
60
|
+
|
61
|
+
# Initialize the API client
|
62
|
+
api = API(username="your_username", token="your_token")
|
63
|
+
|
64
|
+
# Get all properties
|
65
|
+
properties = api.getproperties()
|
66
|
+
for prop in properties:
|
67
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
68
|
+
|
69
|
+
# Get bookings for a property
|
70
|
+
bookings = api.getbookings(
|
71
|
+
property_id=123,
|
72
|
+
since_utc=datetime(2024, 1, 1)
|
73
|
+
)
|
74
|
+
for booking in bookings:
|
75
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
76
|
+
|
77
|
+
# Get a specific booking
|
78
|
+
booking = api.getbooking(booking_id=456)
|
79
|
+
print(f"Guest ID: {booking.guest_id}")
|
80
|
+
|
81
|
+
# Get guest details
|
82
|
+
guest = api.getguest(guest_id=789)
|
83
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
84
|
+
|
85
|
+
# Check if a property is currently booked
|
86
|
+
is_booked = api.isunitbooked(property_id=123)
|
87
|
+
print(f"Property is booked: {is_booked}")
|
88
|
+
```
|
89
|
+
|
90
|
+
## API Documentation
|
91
|
+
|
92
|
+
### API Class Methods
|
93
|
+
|
94
|
+
- `getproperties()` -> List[Property]
|
95
|
+
- Returns a list of all properties
|
96
|
+
|
97
|
+
- `getproperty(property_id: int)` -> Property
|
98
|
+
- Get details for a specific property
|
99
|
+
- Parameters:
|
100
|
+
- property_id: The property ID
|
101
|
+
|
102
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
103
|
+
- Get bookings for a specific property since a given date
|
104
|
+
- Parameters:
|
105
|
+
- property_id: The property ID
|
106
|
+
- since_utc: DateTime object for the start date
|
107
|
+
|
108
|
+
- `getbooking(booking_id: int)` -> Booking
|
109
|
+
- Get details for a specific booking
|
110
|
+
- Parameters:
|
111
|
+
- booking_id: The booking ID
|
112
|
+
|
113
|
+
- `getguest(guest_id: int)` -> Guest
|
114
|
+
- Get details for a specific guest
|
115
|
+
- Parameters:
|
116
|
+
- guest_id: The guest ID
|
117
|
+
|
118
|
+
- `isunitbooked(property_id: int)` -> bool
|
119
|
+
- Check if a property is currently booked
|
120
|
+
- Parameters:
|
121
|
+
- property_id: The property ID
|
122
|
+
|
123
|
+
### Data Models
|
124
|
+
|
125
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
126
|
+
|
127
|
+
Main models:
|
128
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
129
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
130
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
131
|
+
|
132
|
+
Supporting models:
|
133
|
+
- `Address`: Physical address information
|
134
|
+
- `EmailAddress`: Email contact information
|
135
|
+
- `Phone`: Phone contact information
|
136
|
+
- `Charge`: Booking charge information
|
137
|
+
- `DoorCode`: Property access codes
|
138
|
+
|
139
|
+
## CLI Tool (for testing)
|
140
|
+
|
141
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
# Using .env file
|
145
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
146
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
147
|
+
|
148
|
+
# List properties
|
149
|
+
ownerrez properties
|
150
|
+
|
151
|
+
# Get property details
|
152
|
+
ownerrez property 123
|
153
|
+
|
154
|
+
# Check bookings
|
155
|
+
ownerrez bookings 123 --since 2024-01-01
|
156
|
+
|
157
|
+
# Get booking details
|
158
|
+
ownerrez booking 456
|
159
|
+
```
|
160
|
+
|
161
|
+
## Development
|
162
|
+
|
163
|
+
To set up the development environment:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
167
|
+
cd ownerrez-wrapper
|
168
|
+
pip install -e ".[dev]"
|
169
|
+
```
|
170
|
+
|
171
|
+
To run tests:
|
172
|
+
|
173
|
+
```bash
|
174
|
+
pytest
|
175
|
+
```
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
180
|
+
|
181
|
+
## Author
|
182
|
+
|
183
|
+
- Geody Moore (geody.moore@gmail.com)
|
184
|
+
|
185
|
+
## Contributing
|
186
|
+
|
187
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# OwnerRez API Wrapper
|
2
|
+
|
3
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
4
|
+
|
5
|
+
## Requirements
|
6
|
+
|
7
|
+
- Python 3.10 or higher
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
```bash
|
12
|
+
pip install ownerrez-wrapper
|
13
|
+
```
|
14
|
+
|
15
|
+
## Python Usage
|
16
|
+
|
17
|
+
```python
|
18
|
+
from ownerrez_wrapper import API
|
19
|
+
from datetime import datetime
|
20
|
+
|
21
|
+
# Initialize the API client
|
22
|
+
api = API(username="your_username", token="your_token")
|
23
|
+
|
24
|
+
# Get all properties
|
25
|
+
properties = api.getproperties()
|
26
|
+
for prop in properties:
|
27
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
28
|
+
|
29
|
+
# Get bookings for a property
|
30
|
+
bookings = api.getbookings(
|
31
|
+
property_id=123,
|
32
|
+
since_utc=datetime(2024, 1, 1)
|
33
|
+
)
|
34
|
+
for booking in bookings:
|
35
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
36
|
+
|
37
|
+
# Get a specific booking
|
38
|
+
booking = api.getbooking(booking_id=456)
|
39
|
+
print(f"Guest ID: {booking.guest_id}")
|
40
|
+
|
41
|
+
# Get guest details
|
42
|
+
guest = api.getguest(guest_id=789)
|
43
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
44
|
+
|
45
|
+
# Check if a property is currently booked
|
46
|
+
is_booked = api.isunitbooked(property_id=123)
|
47
|
+
print(f"Property is booked: {is_booked}")
|
48
|
+
```
|
49
|
+
|
50
|
+
## API Documentation
|
51
|
+
|
52
|
+
### API Class Methods
|
53
|
+
|
54
|
+
- `getproperties()` -> List[Property]
|
55
|
+
- Returns a list of all properties
|
56
|
+
|
57
|
+
- `getproperty(property_id: int)` -> Property
|
58
|
+
- Get details for a specific property
|
59
|
+
- Parameters:
|
60
|
+
- property_id: The property ID
|
61
|
+
|
62
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
63
|
+
- Get bookings for a specific property since a given date
|
64
|
+
- Parameters:
|
65
|
+
- property_id: The property ID
|
66
|
+
- since_utc: DateTime object for the start date
|
67
|
+
|
68
|
+
- `getbooking(booking_id: int)` -> Booking
|
69
|
+
- Get details for a specific booking
|
70
|
+
- Parameters:
|
71
|
+
- booking_id: The booking ID
|
72
|
+
|
73
|
+
- `getguest(guest_id: int)` -> Guest
|
74
|
+
- Get details for a specific guest
|
75
|
+
- Parameters:
|
76
|
+
- guest_id: The guest ID
|
77
|
+
|
78
|
+
- `isunitbooked(property_id: int)` -> bool
|
79
|
+
- Check if a property is currently booked
|
80
|
+
- Parameters:
|
81
|
+
- property_id: The property ID
|
82
|
+
|
83
|
+
### Data Models
|
84
|
+
|
85
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
86
|
+
|
87
|
+
Main models:
|
88
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
89
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
90
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
91
|
+
|
92
|
+
Supporting models:
|
93
|
+
- `Address`: Physical address information
|
94
|
+
- `EmailAddress`: Email contact information
|
95
|
+
- `Phone`: Phone contact information
|
96
|
+
- `Charge`: Booking charge information
|
97
|
+
- `DoorCode`: Property access codes
|
98
|
+
|
99
|
+
## CLI Tool (for testing)
|
100
|
+
|
101
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
# Using .env file
|
105
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
106
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
107
|
+
|
108
|
+
# List properties
|
109
|
+
ownerrez properties
|
110
|
+
|
111
|
+
# Get property details
|
112
|
+
ownerrez property 123
|
113
|
+
|
114
|
+
# Check bookings
|
115
|
+
ownerrez bookings 123 --since 2024-01-01
|
116
|
+
|
117
|
+
# Get booking details
|
118
|
+
ownerrez booking 456
|
119
|
+
```
|
120
|
+
|
121
|
+
## Development
|
122
|
+
|
123
|
+
To set up the development environment:
|
124
|
+
|
125
|
+
```bash
|
126
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
127
|
+
cd ownerrez-wrapper
|
128
|
+
pip install -e ".[dev]"
|
129
|
+
```
|
130
|
+
|
131
|
+
To run tests:
|
132
|
+
|
133
|
+
```bash
|
134
|
+
pytest
|
135
|
+
```
|
136
|
+
|
137
|
+
## License
|
138
|
+
|
139
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
140
|
+
|
141
|
+
## Author
|
142
|
+
|
143
|
+
- Geody Moore (geody.moore@gmail.com)
|
144
|
+
|
145
|
+
## Contributing
|
146
|
+
|
147
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
OwnerRez API Wrapper
|
3
|
+
~~~~~~~~~~~~~~~~~~~
|
4
|
+
|
5
|
+
A Python wrapper for the OwnerRez API.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .api import API
|
9
|
+
from .cli import main as cli
|
10
|
+
|
11
|
+
__version__ = "0.1.0"
|
12
|
+
__author__ = "Geody Moore"
|
13
|
+
__email__ = "geody.moore@gmail.com"
|
14
|
+
|
15
|
+
__all__ = ["API", "cli"]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from typing import List
|
2
|
+
from datetime import datetime
|
3
|
+
from .restAdapter import RestAdapter
|
4
|
+
from .model import Property, Booking, Guest
|
5
|
+
|
6
|
+
|
7
|
+
class API(object):
|
8
|
+
"""
|
9
|
+
OwnerRez API wrapper class
|
10
|
+
"""
|
11
|
+
|
12
|
+
def __init__(self,username, token):
|
13
|
+
"""
|
14
|
+
Initialize the OwnerRez API wrapper with the OwnerRez username and token
|
15
|
+
:param username: OwnerRez username
|
16
|
+
:param token: OwnerRez token
|
17
|
+
"""
|
18
|
+
self.username = username
|
19
|
+
self.token = token
|
20
|
+
|
21
|
+
|
22
|
+
def getproperties(self) -> list:
|
23
|
+
"""
|
24
|
+
Get a list of properties.
|
25
|
+
"""
|
26
|
+
restAdapt = RestAdapter(self.username,self.token)
|
27
|
+
results = []
|
28
|
+
property_list = restAdapt.get(endpoint='properties')
|
29
|
+
for prop in property_list.data['items']:
|
30
|
+
prop = Property(**prop)
|
31
|
+
results.append(prop)
|
32
|
+
return results
|
33
|
+
|
34
|
+
def getproperty(self, property_id: int) -> Property:
|
35
|
+
"""
|
36
|
+
Get a single property by ID.
|
37
|
+
"""
|
38
|
+
restAdapt = RestAdapter(self.username,self.token)
|
39
|
+
property_data = restAdapt.get(endpoint=f'properties/{property_id}')
|
40
|
+
return Property(**property_data.data)
|
41
|
+
|
42
|
+
def getbookings(self, property_id: int, since_utc: datetime) -> List[Booking]:
|
43
|
+
"""
|
44
|
+
Get a list of bookings for a property since a given date.
|
45
|
+
"""
|
46
|
+
restAdapt = RestAdapter(self.username,self.token)
|
47
|
+
results = []
|
48
|
+
params = {'since_utc': since_utc, 'property_id': property_id}
|
49
|
+
booking_list = restAdapt.get(endpoint='bookings', ep_params=params)
|
50
|
+
|
51
|
+
for booking in booking_list.data['items']:
|
52
|
+
booking = Booking(**booking)
|
53
|
+
results.append(booking)
|
54
|
+
return results
|
55
|
+
|
56
|
+
def getbooking(self, booking_id: int) -> Booking:
|
57
|
+
"""
|
58
|
+
Get a single booking by ID.
|
59
|
+
"""
|
60
|
+
restAdapt = RestAdapter(self.username,self.token)
|
61
|
+
booking = restAdapt.get(endpoint=f'bookings/{booking_id}')
|
62
|
+
return Booking(**booking.data)
|
63
|
+
|
64
|
+
def getguest(self, guest_id: int) -> Guest:
|
65
|
+
"""
|
66
|
+
Get a single guest by ID.
|
67
|
+
"""
|
68
|
+
restAdapt = RestAdapter(self.username,self.token)
|
69
|
+
guest = restAdapt.get(endpoint=f'guests/{guest_id}')
|
70
|
+
return Guest(**guest.data)
|
71
|
+
|
72
|
+
def isunitbooked(self, property_id: int) -> bool:
|
73
|
+
"""
|
74
|
+
Check if a unit is booked today.
|
75
|
+
"""
|
76
|
+
today = datetime.today()
|
77
|
+
bookings = self.getbookings(property_id=property_id, since_utc=today)
|
78
|
+
for booking in bookings:
|
79
|
+
# Convert string dates to datetime if they aren't already
|
80
|
+
arrival = booking.arrival if isinstance(booking.arrival, datetime) else datetime.strptime(booking.arrival, "%Y-%m-%d")
|
81
|
+
departure = booking.departure if isinstance(booking.departure, datetime) else datetime.strptime(booking.departure, "%Y-%m-%d")
|
82
|
+
if arrival <= today and departure >= today:
|
83
|
+
return True
|
84
|
+
return False
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import argparse
|
3
|
+
from datetime import datetime
|
4
|
+
import json
|
5
|
+
from typing import Any
|
6
|
+
import os
|
7
|
+
from pathlib import Path
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
from .api import API
|
10
|
+
from uuid import UUID
|
11
|
+
|
12
|
+
def json_serial(obj: Any) -> str:
|
13
|
+
"""JSON serializer for objects not serializable by default json code"""
|
14
|
+
if isinstance(obj, datetime):
|
15
|
+
return obj.isoformat()
|
16
|
+
if isinstance(obj, UUID):
|
17
|
+
return str(obj)
|
18
|
+
if hasattr(obj, '__dict__'):
|
19
|
+
return {k: v for k, v in obj.__dict__.items() if v is not None}
|
20
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
21
|
+
|
22
|
+
def print_json(data: Any) -> None:
|
23
|
+
"""Print data as formatted JSON"""
|
24
|
+
print(json.dumps(data, indent=2, default=json_serial))
|
25
|
+
|
26
|
+
def load_env():
|
27
|
+
"""Load environment variables from .env file"""
|
28
|
+
env_path = Path('.env')
|
29
|
+
if env_path.exists():
|
30
|
+
load_dotenv(env_path)
|
31
|
+
|
32
|
+
def main():
|
33
|
+
# Load environment variables from .env file
|
34
|
+
load_env()
|
35
|
+
|
36
|
+
parser = argparse.ArgumentParser(description='OwnerRez API CLI')
|
37
|
+
|
38
|
+
# Authentication arguments
|
39
|
+
parser.add_argument('--username', help='OwnerRez API username')
|
40
|
+
parser.add_argument('--token', help='OwnerRez API token')
|
41
|
+
|
42
|
+
# Commands
|
43
|
+
subparsers = parser.add_subparsers(dest='command', help='Commands')
|
44
|
+
|
45
|
+
# Properties command
|
46
|
+
properties_parser = subparsers.add_parser('properties', help='List all properties')
|
47
|
+
|
48
|
+
# Property command
|
49
|
+
property_parser = subparsers.add_parser('property', help='Get a specific property')
|
50
|
+
property_parser.add_argument('property_id', type=int, help='Property ID')
|
51
|
+
|
52
|
+
# Bookings command
|
53
|
+
bookings_parser = subparsers.add_parser('bookings', help='Get bookings for a property')
|
54
|
+
bookings_parser.add_argument('property_id', type=int, help='Property ID')
|
55
|
+
bookings_parser.add_argument('--since', type=str, help='Get bookings since date (YYYY-MM-DD)')
|
56
|
+
|
57
|
+
# Booking command
|
58
|
+
booking_parser = subparsers.add_parser('booking', help='Get a specific booking')
|
59
|
+
booking_parser.add_argument('booking_id', type=int, help='Booking ID')
|
60
|
+
|
61
|
+
# Guest command
|
62
|
+
guest_parser = subparsers.add_parser('guest', help='Get a specific guest')
|
63
|
+
guest_parser.add_argument('guest_id', type=int, help='Guest ID')
|
64
|
+
|
65
|
+
# Check if unit is booked command
|
66
|
+
booked_parser = subparsers.add_parser('is-booked', help='Check if a unit is currently booked')
|
67
|
+
booked_parser.add_argument('property_id', type=int, help='Property ID')
|
68
|
+
|
69
|
+
args = parser.parse_args()
|
70
|
+
|
71
|
+
# Get credentials from arguments or environment
|
72
|
+
username = args.username or os.environ.get('OWNERREZ_USERNAME')
|
73
|
+
token = args.token or os.environ.get('OWNERREZ_TOKEN')
|
74
|
+
|
75
|
+
if not username or not token:
|
76
|
+
parser.error('Username and token must be provided via arguments, environment variables, or .env file')
|
77
|
+
|
78
|
+
api = API(username=username, token=token)
|
79
|
+
|
80
|
+
try:
|
81
|
+
if args.command == 'properties':
|
82
|
+
properties = api.getproperties()
|
83
|
+
print_json(properties)
|
84
|
+
|
85
|
+
elif args.command == 'property':
|
86
|
+
property = api.getproperty(property_id=args.property_id)
|
87
|
+
print_json(property)
|
88
|
+
|
89
|
+
elif args.command == 'bookings':
|
90
|
+
since_date = datetime.strptime(args.since, "%Y-%m-%d") if args.since else datetime.today()
|
91
|
+
bookings = api.getbookings(property_id=args.property_id, since_utc=since_date)
|
92
|
+
print_json(bookings)
|
93
|
+
|
94
|
+
elif args.command == 'booking':
|
95
|
+
booking = api.getbooking(booking_id=args.booking_id)
|
96
|
+
print_json(booking)
|
97
|
+
|
98
|
+
elif args.command == 'guest':
|
99
|
+
guest = api.getguest(guest_id=args.guest_id)
|
100
|
+
print_json(guest)
|
101
|
+
|
102
|
+
elif args.command == 'is-booked':
|
103
|
+
is_booked = api.isunitbooked(property_id=args.property_id)
|
104
|
+
print_json({"is_booked": is_booked})
|
105
|
+
|
106
|
+
else:
|
107
|
+
parser.print_help()
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
print(f"Error: {str(e)}")
|
111
|
+
exit(1)
|
112
|
+
|
113
|
+
if __name__ == '__main__':
|
114
|
+
main()
|
@@ -0,0 +1 @@
|
|
1
|
+
BASEURL = "https://api.ownerreservations.com/v2"
|
@@ -0,0 +1,218 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from uuid import UUID
|
3
|
+
from datetime import datetime
|
4
|
+
from typing import List, Dict, Optional, Any
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class Result:
|
9
|
+
message: str
|
10
|
+
status: str
|
11
|
+
data: Optional[List[Dict]] = None
|
12
|
+
|
13
|
+
def __init__(self, **kwargs):
|
14
|
+
for key, value in kwargs.items():
|
15
|
+
if hasattr(self, key):
|
16
|
+
setattr(self, key, value)
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class Address:
|
20
|
+
city: Optional[str] = None
|
21
|
+
country: Optional[str] = None
|
22
|
+
id: Optional[int] = None
|
23
|
+
is_default: Optional[bool] = None
|
24
|
+
postal_code: Optional[str] = None
|
25
|
+
province: Optional[str] = None
|
26
|
+
state: Optional[str] = None
|
27
|
+
street1: Optional[str] = None
|
28
|
+
street2: Optional[str] = None
|
29
|
+
type: Optional[str] = None
|
30
|
+
|
31
|
+
def __init__(self, **kwargs):
|
32
|
+
for key, value in kwargs.items():
|
33
|
+
if hasattr(self, key):
|
34
|
+
setattr(self, key, value)
|
35
|
+
|
36
|
+
@dataclass
|
37
|
+
class EmailAddress:
|
38
|
+
address: Optional[str] = None
|
39
|
+
id: Optional[int] = None
|
40
|
+
is_default: Optional[bool] = None
|
41
|
+
type: Optional[str] = None
|
42
|
+
|
43
|
+
def __init__(self, **kwargs):
|
44
|
+
for key, value in kwargs.items():
|
45
|
+
if hasattr(self, key):
|
46
|
+
setattr(self, key, value)
|
47
|
+
|
48
|
+
@dataclass
|
49
|
+
class Phone:
|
50
|
+
extension: Optional[str] = None
|
51
|
+
id: Optional[int] = None
|
52
|
+
is_default: Optional[bool] = None
|
53
|
+
number: Optional[str] = None
|
54
|
+
type: Optional[str] = None
|
55
|
+
|
56
|
+
def __init__(self, **kwargs):
|
57
|
+
for key, value in kwargs.items():
|
58
|
+
if hasattr(self, key):
|
59
|
+
setattr(self, key, value)
|
60
|
+
|
61
|
+
@dataclass
|
62
|
+
class Guest:
|
63
|
+
addresses: Optional[List[Address]] = None
|
64
|
+
email_addresses: Optional[List[EmailAddress]] = None
|
65
|
+
first_name: Optional[str] = None
|
66
|
+
id: Optional[int] = None
|
67
|
+
last_name: Optional[str] = None
|
68
|
+
notes: Optional[str] = None
|
69
|
+
phones: Optional[List[Phone]] = None
|
70
|
+
|
71
|
+
def __init__(self, **kwargs):
|
72
|
+
for key, value in kwargs.items():
|
73
|
+
if hasattr(self, key):
|
74
|
+
if key == 'addresses' and value:
|
75
|
+
self.addresses = [Address(**addr) for addr in value]
|
76
|
+
elif key == 'email_addresses' and value:
|
77
|
+
self.email_addresses = [EmailAddress(**email) for email in value]
|
78
|
+
elif key == 'phones' and value:
|
79
|
+
self.phones = [Phone(**phone) for phone in value]
|
80
|
+
else:
|
81
|
+
setattr(self, key, value)
|
82
|
+
|
83
|
+
@dataclass
|
84
|
+
class Property:
|
85
|
+
active: Optional[bool] = None
|
86
|
+
address: Optional[Address] = None
|
87
|
+
bathrooms: Optional[int] = None
|
88
|
+
bathrooms_full: Optional[int] = None
|
89
|
+
bathrooms_half: Optional[int] = None
|
90
|
+
bedrooms: Optional[int] = None
|
91
|
+
check_in: Optional[str] = None
|
92
|
+
check_in_end: Optional[str] = None
|
93
|
+
check_out: Optional[str] = None
|
94
|
+
currency_code: Optional[str] = None
|
95
|
+
display_order: Optional[int] = None
|
96
|
+
external_display_order: Optional[int] = None
|
97
|
+
external_name: Optional[str] = None
|
98
|
+
id: Optional[int] = None
|
99
|
+
internal_code: Optional[str] = None
|
100
|
+
key: Optional[UUID] = None
|
101
|
+
latitude: Optional[int] = None
|
102
|
+
longitude: Optional[int] = None
|
103
|
+
max_adults: Optional[int] = None
|
104
|
+
max_children: Optional[int] = None
|
105
|
+
max_guests: Optional[int] = None
|
106
|
+
max_pets: Optional[int] = None
|
107
|
+
name: Optional[str] = None
|
108
|
+
owner_id: Optional[int] = None
|
109
|
+
property_type: Optional[str] = None
|
110
|
+
public_url: Optional[str] = None
|
111
|
+
thumbnail_url: Optional[str] = None
|
112
|
+
thumbnail_url_large: Optional[str] = None
|
113
|
+
thumbnail_url_medium: Optional[str] = None
|
114
|
+
living_area: Optional[int] = None
|
115
|
+
|
116
|
+
def __init__(self, **kwargs):
|
117
|
+
for key, value in kwargs.items():
|
118
|
+
if hasattr(self, key):
|
119
|
+
if key == 'address' and value:
|
120
|
+
self.address = Address(**value)
|
121
|
+
else:
|
122
|
+
setattr(self, key, value)
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
@dataclass
|
127
|
+
class Charge:
|
128
|
+
amount: Optional[int] = None
|
129
|
+
commission_amount: Optional[int] = None
|
130
|
+
description: Optional[str] = None
|
131
|
+
expense_id: Optional[int] = None
|
132
|
+
is_channel_managed: Optional[bool] = None
|
133
|
+
is_commission_all: Optional[bool] = None
|
134
|
+
is_expense_all: Optional[bool] = None
|
135
|
+
is_taxable: Optional[bool] = None
|
136
|
+
owner_amount: Optional[int] = None
|
137
|
+
owner_commission_percent: Optional[int] = None
|
138
|
+
position: Optional[int] = None
|
139
|
+
rate: Optional[int] = None
|
140
|
+
rate_is_percent: Optional[bool] = None
|
141
|
+
surcharge_id: Optional[int] = None
|
142
|
+
tax_id: Optional[int] = None
|
143
|
+
type: Optional[str] = None
|
144
|
+
|
145
|
+
def __init__(self, **kwargs):
|
146
|
+
for key, value in kwargs.items():
|
147
|
+
if hasattr(self, key):
|
148
|
+
setattr(self, key, value)
|
149
|
+
|
150
|
+
|
151
|
+
@dataclass
|
152
|
+
class DoorCode:
|
153
|
+
code: Optional[str] = None
|
154
|
+
lock_names: Optional[str] = None
|
155
|
+
|
156
|
+
def __init__(self, **kwargs):
|
157
|
+
for key, value in kwargs.items():
|
158
|
+
if hasattr(self, key):
|
159
|
+
setattr(self, key, value)
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
@dataclass
|
164
|
+
class Booking:
|
165
|
+
adults: Optional[int] = None
|
166
|
+
arrival: Optional[datetime] = None
|
167
|
+
booked_utc: Optional[datetime] = None
|
168
|
+
canceled_utc: Optional[datetime] = None
|
169
|
+
charges: Optional[List[Charge]] = None
|
170
|
+
check_in: Optional[str] = None
|
171
|
+
check_in_end: Optional[str] = None
|
172
|
+
check_out: Optional[str] = None
|
173
|
+
children: Optional[int] = None
|
174
|
+
cleaning_date: Optional[datetime] = None
|
175
|
+
currency_code: Optional[str] = None
|
176
|
+
departure: Optional[datetime] = None
|
177
|
+
door_codes: Optional[List[DoorCode]] = None
|
178
|
+
form_key: Optional[str] = None
|
179
|
+
guest: Optional[Guest] = None
|
180
|
+
guest_id: Optional[int] = None
|
181
|
+
id: Optional[int] = None
|
182
|
+
infants: Optional[int] = None
|
183
|
+
is_block: Optional[bool] = None
|
184
|
+
listing_site: Optional[str] = None
|
185
|
+
notes: Optional[str] = None
|
186
|
+
owner_id: Optional[int] = None
|
187
|
+
pending_until_utc: Optional[datetime] = None
|
188
|
+
pets: Optional[int] = None
|
189
|
+
platform_email_address: Optional[str] = None
|
190
|
+
platform_reservation_number: Optional[str] = None
|
191
|
+
property_id: Optional[int] = None
|
192
|
+
quote_id: Optional[int] = None
|
193
|
+
status: Optional[str] = None
|
194
|
+
title: Optional[str] = None
|
195
|
+
total_amount: Optional[int] = None
|
196
|
+
total_host_fees: Optional[int] = None
|
197
|
+
total_owed: Optional[int] = None
|
198
|
+
total_paid: Optional[int] = None
|
199
|
+
total_refunded: Optional[int] = None
|
200
|
+
type: Optional[str] = None
|
201
|
+
|
202
|
+
def __init__(self, **kwargs):
|
203
|
+
for key, value in kwargs.items():
|
204
|
+
if hasattr(self, key):
|
205
|
+
if key in ['arrival', 'departure', 'booked_utc', 'canceled_utc', 'cleaning_date', 'pending_until_utc'] and value:
|
206
|
+
# Convert string dates to datetime objects
|
207
|
+
try:
|
208
|
+
setattr(self, key, datetime.fromisoformat(value.replace('Z', '+00:00')))
|
209
|
+
except (ValueError, AttributeError):
|
210
|
+
setattr(self, key, value)
|
211
|
+
elif key == 'charges' and value:
|
212
|
+
self.charges = [Charge(**charge) for charge in value]
|
213
|
+
elif key == 'door_codes' and value:
|
214
|
+
self.door_codes = [DoorCode(**code) for code in value]
|
215
|
+
elif key == 'guest' and value:
|
216
|
+
self.guest = Guest(**value)
|
217
|
+
else:
|
218
|
+
setattr(self, key, value)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import requests
|
2
|
+
from json import JSONDecodeError
|
3
|
+
from ownerrez_wrapper.model import Result
|
4
|
+
import base64
|
5
|
+
from typing import List, Dict
|
6
|
+
import logging
|
7
|
+
import datetime
|
8
|
+
from ownerrez_wrapper.constants import BASEURL as hosturl
|
9
|
+
from ownerrez_wrapper.exceptions import OwnerrezApiException
|
10
|
+
|
11
|
+
class RestAdapter(object):
|
12
|
+
def __init__(self,username, token):
|
13
|
+
"""
|
14
|
+
Initialize the RestAdapter object with the OwnerRez username and token
|
15
|
+
:param username: OwnerRez username
|
16
|
+
:param token: OwnerRez token
|
17
|
+
"""
|
18
|
+
auth_string = f"{username}:{token}"
|
19
|
+
auth_bytes = auth_string.encode('ascii')
|
20
|
+
auth_b64 = base64.b64encode(auth_bytes).decode('ascii')
|
21
|
+
self.headers = {
|
22
|
+
'User-Agent': 'OwnerRezAPI',
|
23
|
+
'Content-Type': 'application/json',
|
24
|
+
'Authorization': f'Basic {auth_b64}'
|
25
|
+
}
|
26
|
+
self._logger = logging.getLogger(__name__)
|
27
|
+
self._logger.setLevel(logging.DEBUG)
|
28
|
+
|
29
|
+
def _do_request(self, http_method, endpoint, ep_params, data: Dict = None):
|
30
|
+
"""
|
31
|
+
Make a request to the OwnerRez API
|
32
|
+
:param http_method: HTTP method to use
|
33
|
+
:param endpoint: API endpoint to call
|
34
|
+
:param ep_params: Dictionary of parameters to pass to the endpoint
|
35
|
+
:param data: Dictionary of data to pass in the request body
|
36
|
+
:return: Result object
|
37
|
+
"""
|
38
|
+
|
39
|
+
full_url = f"{hosturl}/{endpoint.lstrip('/')}"
|
40
|
+
log_line_pre = f"method={http_method}, url={full_url}, params={ep_params}"
|
41
|
+
log_line_post = ', '.join((log_line_pre, "success={}, status_code={}, message={}"))
|
42
|
+
|
43
|
+
# Make the request
|
44
|
+
try:
|
45
|
+
self._logger.debug(msg = log_line_pre)
|
46
|
+
response = requests.request(method=http_method, url=full_url, headers=self.headers, params=ep_params, json=data)
|
47
|
+
|
48
|
+
# If the request fails, log the error and raise an exception
|
49
|
+
except requests.exceptions.RequestException as e:
|
50
|
+
self._logger.error(msg=(str(e)))
|
51
|
+
raise OwnerrezApiException("Request failed") from e
|
52
|
+
|
53
|
+
# Check status code first
|
54
|
+
is_success = 299 >= response.status_code >= 200 # 200 to 299 is OK
|
55
|
+
if not is_success:
|
56
|
+
self._logger.error(msg=log_line_post.format(False, response.status_code, response.reason))
|
57
|
+
raise OwnerrezApiException(f"{response.status_code}: {response.reason}")
|
58
|
+
|
59
|
+
# Try to parse the response as JSON only for successful responses
|
60
|
+
try:
|
61
|
+
data_out = response.json()
|
62
|
+
except (ValueError, TypeError, JSONDecodeError) as e:
|
63
|
+
self._logger.error(msg=log_line_post.format(False, response.status_code, e))
|
64
|
+
raise OwnerrezApiException("Bad JSON in response") from e
|
65
|
+
|
66
|
+
|
67
|
+
return Result(status=response.status_code, message=response.reason, data=data_out)
|
68
|
+
|
69
|
+
def get(self, endpoint: str, ep_params: Dict = None) -> Result:
|
70
|
+
"""
|
71
|
+
Make a GET request to the OwnerRez API
|
72
|
+
:param endpoint: API endpoint to call
|
73
|
+
:param ep_params: Dictionary of parameters to pass to the endpoint
|
74
|
+
:return: Result object
|
75
|
+
"""
|
76
|
+
return self._do_request(http_method='GET', endpoint=endpoint, ep_params=ep_params)
|
@@ -0,0 +1,187 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: ownerrez-wrapper
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python wrapper for the OwnerRez API
|
5
|
+
Home-page: https://github.com/gmoorevt/ownerrez-wrapper
|
6
|
+
Author: Geody Moore
|
7
|
+
Author-email: geody.moore@gmail.com
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Developers
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
11
|
+
Classifier: Operating System :: OS Independent
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Requires-Python: >=3.10
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
License-File: LICENSE
|
19
|
+
Requires-Dist: requests>=2.25.0
|
20
|
+
Requires-Dist: python-dateutil>=2.8.0
|
21
|
+
Requires-Dist: python-dotenv>=1.0.0
|
22
|
+
Provides-Extra: dev
|
23
|
+
Requires-Dist: pytest>=6.0.0; extra == "dev"
|
24
|
+
Requires-Dist: pytest-cov>=2.0.0; extra == "dev"
|
25
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
26
|
+
Requires-Dist: isort>=5.0.0; extra == "dev"
|
27
|
+
Requires-Dist: flake8>=3.9.0; extra == "dev"
|
28
|
+
Requires-Dist: mypy>=0.900; extra == "dev"
|
29
|
+
Requires-Dist: freezegun>=1.2.0; extra == "dev"
|
30
|
+
Dynamic: author
|
31
|
+
Dynamic: author-email
|
32
|
+
Dynamic: classifier
|
33
|
+
Dynamic: description
|
34
|
+
Dynamic: description-content-type
|
35
|
+
Dynamic: home-page
|
36
|
+
Dynamic: provides-extra
|
37
|
+
Dynamic: requires-dist
|
38
|
+
Dynamic: requires-python
|
39
|
+
Dynamic: summary
|
40
|
+
|
41
|
+
# OwnerRez API Wrapper
|
42
|
+
|
43
|
+
A Python wrapper for the OwnerRez API. This package provides a simple and intuitive interface to interact with the OwnerRez API v2.
|
44
|
+
|
45
|
+
## Requirements
|
46
|
+
|
47
|
+
- Python 3.10 or higher
|
48
|
+
|
49
|
+
## Installation
|
50
|
+
|
51
|
+
```bash
|
52
|
+
pip install ownerrez-wrapper
|
53
|
+
```
|
54
|
+
|
55
|
+
## Python Usage
|
56
|
+
|
57
|
+
```python
|
58
|
+
from ownerrez_wrapper import API
|
59
|
+
from datetime import datetime
|
60
|
+
|
61
|
+
# Initialize the API client
|
62
|
+
api = API(username="your_username", token="your_token")
|
63
|
+
|
64
|
+
# Get all properties
|
65
|
+
properties = api.getproperties()
|
66
|
+
for prop in properties:
|
67
|
+
print(f"Property: {prop.name} (ID: {prop.id})")
|
68
|
+
|
69
|
+
# Get bookings for a property
|
70
|
+
bookings = api.getbookings(
|
71
|
+
property_id=123,
|
72
|
+
since_utc=datetime(2024, 1, 1)
|
73
|
+
)
|
74
|
+
for booking in bookings:
|
75
|
+
print(f"Booking: {booking.arrival} to {booking.departure}")
|
76
|
+
|
77
|
+
# Get a specific booking
|
78
|
+
booking = api.getbooking(booking_id=456)
|
79
|
+
print(f"Guest ID: {booking.guest_id}")
|
80
|
+
|
81
|
+
# Get guest details
|
82
|
+
guest = api.getguest(guest_id=789)
|
83
|
+
print(f"Guest: {guest.first_name} {guest.last_name}")
|
84
|
+
|
85
|
+
# Check if a property is currently booked
|
86
|
+
is_booked = api.isunitbooked(property_id=123)
|
87
|
+
print(f"Property is booked: {is_booked}")
|
88
|
+
```
|
89
|
+
|
90
|
+
## API Documentation
|
91
|
+
|
92
|
+
### API Class Methods
|
93
|
+
|
94
|
+
- `getproperties()` -> List[Property]
|
95
|
+
- Returns a list of all properties
|
96
|
+
|
97
|
+
- `getproperty(property_id: int)` -> Property
|
98
|
+
- Get details for a specific property
|
99
|
+
- Parameters:
|
100
|
+
- property_id: The property ID
|
101
|
+
|
102
|
+
- `getbookings(property_id: int, since_utc: datetime)` -> List[Booking]
|
103
|
+
- Get bookings for a specific property since a given date
|
104
|
+
- Parameters:
|
105
|
+
- property_id: The property ID
|
106
|
+
- since_utc: DateTime object for the start date
|
107
|
+
|
108
|
+
- `getbooking(booking_id: int)` -> Booking
|
109
|
+
- Get details for a specific booking
|
110
|
+
- Parameters:
|
111
|
+
- booking_id: The booking ID
|
112
|
+
|
113
|
+
- `getguest(guest_id: int)` -> Guest
|
114
|
+
- Get details for a specific guest
|
115
|
+
- Parameters:
|
116
|
+
- guest_id: The guest ID
|
117
|
+
|
118
|
+
- `isunitbooked(property_id: int)` -> bool
|
119
|
+
- Check if a property is currently booked
|
120
|
+
- Parameters:
|
121
|
+
- property_id: The property ID
|
122
|
+
|
123
|
+
### Data Models
|
124
|
+
|
125
|
+
The API returns strongly-typed objects that provide a rich interface to the OwnerRez data. For detailed documentation of all data models, including all available fields and usage examples, see [Data Models Documentation](docs/models.md).
|
126
|
+
|
127
|
+
Main models:
|
128
|
+
- `Property`: Contains property details (id, name, bedrooms, etc.)
|
129
|
+
- `Booking`: Contains booking information (arrival, departure, guest, etc.)
|
130
|
+
- `Guest`: Contains guest information (name, contact details, etc.)
|
131
|
+
|
132
|
+
Supporting models:
|
133
|
+
- `Address`: Physical address information
|
134
|
+
- `EmailAddress`: Email contact information
|
135
|
+
- `Phone`: Phone contact information
|
136
|
+
- `Charge`: Booking charge information
|
137
|
+
- `DoorCode`: Property access codes
|
138
|
+
|
139
|
+
## CLI Tool (for testing)
|
140
|
+
|
141
|
+
A command-line interface is included for testing and debugging. You can provide credentials via environment variables or a `.env` file:
|
142
|
+
|
143
|
+
```bash
|
144
|
+
# Using .env file
|
145
|
+
echo "OWNERREZ_USERNAME=your_username" > .env
|
146
|
+
echo "OWNERREZ_TOKEN=your_token" >> .env
|
147
|
+
|
148
|
+
# List properties
|
149
|
+
ownerrez properties
|
150
|
+
|
151
|
+
# Get property details
|
152
|
+
ownerrez property 123
|
153
|
+
|
154
|
+
# Check bookings
|
155
|
+
ownerrez bookings 123 --since 2024-01-01
|
156
|
+
|
157
|
+
# Get booking details
|
158
|
+
ownerrez booking 456
|
159
|
+
```
|
160
|
+
|
161
|
+
## Development
|
162
|
+
|
163
|
+
To set up the development environment:
|
164
|
+
|
165
|
+
```bash
|
166
|
+
git clone https://github.com/gmoorevt/ownerrez-wrapper.git
|
167
|
+
cd ownerrez-wrapper
|
168
|
+
pip install -e ".[dev]"
|
169
|
+
```
|
170
|
+
|
171
|
+
To run tests:
|
172
|
+
|
173
|
+
```bash
|
174
|
+
pytest
|
175
|
+
```
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
180
|
+
|
181
|
+
## Author
|
182
|
+
|
183
|
+
- Geody Moore (geody.moore@gmail.com)
|
184
|
+
|
185
|
+
## Contributing
|
186
|
+
|
187
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
LICENSE
|
2
|
+
MANIFEST.in
|
3
|
+
README.md
|
4
|
+
requirements-dev.txt
|
5
|
+
requirements.txt
|
6
|
+
setup.py
|
7
|
+
ownerrez_wrapper/__init__.py
|
8
|
+
ownerrez_wrapper/api.py
|
9
|
+
ownerrez_wrapper/cli.py
|
10
|
+
ownerrez_wrapper/constants.py
|
11
|
+
ownerrez_wrapper/exceptions.py
|
12
|
+
ownerrez_wrapper/model.py
|
13
|
+
ownerrez_wrapper/restAdapter.py
|
14
|
+
ownerrez_wrapper.egg-info/PKG-INFO
|
15
|
+
ownerrez_wrapper.egg-info/SOURCES.txt
|
16
|
+
ownerrez_wrapper.egg-info/dependency_links.txt
|
17
|
+
ownerrez_wrapper.egg-info/entry_points.txt
|
18
|
+
ownerrez_wrapper.egg-info/requires.txt
|
19
|
+
ownerrez_wrapper.egg-info/top_level.txt
|
20
|
+
tests/__init__.py
|
21
|
+
tests/test_api.py
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
from setuptools import setup, find_packages
|
2
|
+
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
4
|
+
long_description = fh.read()
|
5
|
+
|
6
|
+
setup(
|
7
|
+
name="ownerrez-wrapper",
|
8
|
+
version="0.1.0",
|
9
|
+
author="Geody Moore",
|
10
|
+
author_email="geody.moore@gmail.com",
|
11
|
+
description="A Python wrapper for the OwnerRez API",
|
12
|
+
long_description=long_description,
|
13
|
+
long_description_content_type="text/markdown",
|
14
|
+
url="https://github.com/gmoorevt/ownerrez-wrapper",
|
15
|
+
packages=find_packages(),
|
16
|
+
classifiers=[
|
17
|
+
"Development Status :: 3 - Alpha",
|
18
|
+
"Intended Audience :: Developers",
|
19
|
+
"License :: OSI Approved :: MIT License",
|
20
|
+
"Operating System :: OS Independent",
|
21
|
+
"Programming Language :: Python :: 3",
|
22
|
+
"Programming Language :: Python :: 3.10",
|
23
|
+
"Programming Language :: Python :: 3.11",
|
24
|
+
"Programming Language :: Python :: 3.12",
|
25
|
+
],
|
26
|
+
python_requires=">=3.10",
|
27
|
+
install_requires=[
|
28
|
+
"requests>=2.25.0",
|
29
|
+
"python-dateutil>=2.8.0",
|
30
|
+
"python-dotenv>=1.0.0"
|
31
|
+
],
|
32
|
+
extras_require={
|
33
|
+
"dev": [
|
34
|
+
"pytest>=6.0.0",
|
35
|
+
"pytest-cov>=2.0.0",
|
36
|
+
"black>=22.0.0",
|
37
|
+
"isort>=5.0.0",
|
38
|
+
"flake8>=3.9.0",
|
39
|
+
"mypy>=0.900",
|
40
|
+
"freezegun>=1.2.0"
|
41
|
+
]
|
42
|
+
},
|
43
|
+
entry_points={
|
44
|
+
'console_scripts': [
|
45
|
+
'ownerrez=ownerrez_wrapper.cli:main',
|
46
|
+
],
|
47
|
+
},
|
48
|
+
)
|
File without changes
|
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
Tests for the OwnerRez API wrapper
|
3
|
+
"""
|
4
|
+
import pytest
|
5
|
+
from datetime import datetime
|
6
|
+
from freezegun import freeze_time
|
7
|
+
from ownerrez_wrapper.api import API
|
8
|
+
from ownerrez_wrapper.model import Result, Property, Booking, Guest
|
9
|
+
|
10
|
+
@pytest.fixture
|
11
|
+
def api():
|
12
|
+
"""Create an API instance for testing"""
|
13
|
+
return API(username="test_user", token="test_token")
|
14
|
+
|
15
|
+
@pytest.fixture
|
16
|
+
def mock_property_response():
|
17
|
+
return {
|
18
|
+
"items": [{
|
19
|
+
"id": 1,
|
20
|
+
"name": "Test Property",
|
21
|
+
"bedrooms": 3,
|
22
|
+
"bathrooms": 2,
|
23
|
+
"active": True
|
24
|
+
}]
|
25
|
+
}
|
26
|
+
|
27
|
+
@pytest.fixture
|
28
|
+
def mock_booking_response():
|
29
|
+
return {
|
30
|
+
"items": [{
|
31
|
+
"id": 1,
|
32
|
+
"property_id": 1,
|
33
|
+
"arrival": "2024-03-01",
|
34
|
+
"departure": "2024-03-05",
|
35
|
+
"status": "confirmed",
|
36
|
+
"guest_id": 1
|
37
|
+
}]
|
38
|
+
}
|
39
|
+
|
40
|
+
@pytest.fixture
|
41
|
+
def mock_guest_response():
|
42
|
+
return {
|
43
|
+
"id": 1,
|
44
|
+
"first_name": "John",
|
45
|
+
"last_name": "Doe",
|
46
|
+
"email_addresses": [{"address": "john@example.com", "is_default": True}]
|
47
|
+
}
|
48
|
+
|
49
|
+
def test_api_initialization(api):
|
50
|
+
"""Test API initialization"""
|
51
|
+
assert api.username == "test_user"
|
52
|
+
assert api.token == "test_token"
|
53
|
+
|
54
|
+
def test_getproperties(api, mock_property_response, monkeypatch):
|
55
|
+
def mock_get(*args, **kwargs):
|
56
|
+
return Result(status=200, message="OK", data=mock_property_response)
|
57
|
+
|
58
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
59
|
+
|
60
|
+
properties = api.getproperties()
|
61
|
+
assert len(properties) == 1
|
62
|
+
assert isinstance(properties[0], Property)
|
63
|
+
assert properties[0].id == 1
|
64
|
+
assert properties[0].name == "Test Property"
|
65
|
+
|
66
|
+
def test_getbookings(api, mock_booking_response, monkeypatch):
|
67
|
+
def mock_get(*args, **kwargs):
|
68
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
69
|
+
|
70
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
71
|
+
|
72
|
+
bookings = api.getbookings(property_id=1, since_utc=datetime(2024, 1, 1))
|
73
|
+
assert len(bookings) == 1
|
74
|
+
assert isinstance(bookings[0], Booking)
|
75
|
+
assert bookings[0].id == 1
|
76
|
+
assert bookings[0].property_id == 1
|
77
|
+
|
78
|
+
def test_getbooking(api, monkeypatch):
|
79
|
+
booking_data = {
|
80
|
+
"id": 1,
|
81
|
+
"property_id": 1,
|
82
|
+
"arrival": "2024-03-01",
|
83
|
+
"departure": "2024-03-05"
|
84
|
+
}
|
85
|
+
|
86
|
+
def mock_get(*args, **kwargs):
|
87
|
+
return Result(status=200, message="OK", data=booking_data)
|
88
|
+
|
89
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
90
|
+
|
91
|
+
booking = api.getbooking(booking_id=1)
|
92
|
+
assert isinstance(booking, Booking)
|
93
|
+
assert booking.id == 1
|
94
|
+
assert booking.property_id == 1
|
95
|
+
|
96
|
+
def test_getguest(api, mock_guest_response, monkeypatch):
|
97
|
+
def mock_get(*args, **kwargs):
|
98
|
+
return Result(status=200, message="OK", data=mock_guest_response)
|
99
|
+
|
100
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
101
|
+
|
102
|
+
guest = api.getguest(guest_id=1)
|
103
|
+
assert isinstance(guest, Guest)
|
104
|
+
assert guest.id == 1
|
105
|
+
assert guest.first_name == "John"
|
106
|
+
assert guest.last_name == "Doe"
|
107
|
+
|
108
|
+
@freeze_time("2024-03-03") # A date between arrival and departure
|
109
|
+
def test_isunitbooked(api, mock_booking_response, monkeypatch):
|
110
|
+
def mock_get(*args, **kwargs):
|
111
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
112
|
+
|
113
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
114
|
+
is_booked = api.isunitbooked(property_id=1)
|
115
|
+
assert is_booked is True
|
116
|
+
|
117
|
+
@freeze_time("2024-03-06") # A date after departure
|
118
|
+
def test_isunitbooked_not_booked(api, mock_booking_response, monkeypatch):
|
119
|
+
def mock_get(*args, **kwargs):
|
120
|
+
return Result(status=200, message="OK", data=mock_booking_response)
|
121
|
+
|
122
|
+
monkeypatch.setattr("ownerrez_wrapper.restAdapter.RestAdapter.get", mock_get)
|
123
|
+
is_booked = api.isunitbooked(property_id=1)
|
124
|
+
assert is_booked is False
|